From 03458a4e6ddc9519a2566a1a7f89a15d2c0fdcfa Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Fri, 25 Oct 2024 11:33:48 +0200 Subject: [PATCH 01/20] implement board creation in room --- .../src/modules/board/board-api.module.ts | 3 +- ....ts => board-create-in-course.api.spec.ts} | 2 +- .../api-test/board-create-in-room.api.spec.ts | 294 ++++++++++++++++++ .../domain/types/board-external-reference.ts | 7 +- apps/server/src/modules/board/uc/board.uc.ts | 32 +- 5 files changed, 325 insertions(+), 13 deletions(-) rename apps/server/src/modules/board/controller/api-test/{board-create.api.spec.ts => board-create-in-course.api.spec.ts} (99%) create mode 100644 apps/server/src/modules/board/controller/api-test/board-create-in-room.api.spec.ts diff --git a/apps/server/src/modules/board/board-api.module.ts b/apps/server/src/modules/board/board-api.module.ts index 3bc604d9ecf..98d6de3acfc 100644 --- a/apps/server/src/modules/board/board-api.module.ts +++ b/apps/server/src/modules/board/board-api.module.ts @@ -12,9 +12,10 @@ import { import { BoardModule } from './board.module'; import { BoardNodePermissionService } from './service'; import { BoardUc, CardUc, ColumnUc, ElementUc, SubmissionItemUc } from './uc'; +import { RoomMemberModule } from '../room-member'; @Module({ - imports: [BoardModule, LoggerModule, forwardRef(() => AuthorizationModule)], + imports: [BoardModule, LoggerModule, RoomMemberModule, forwardRef(() => AuthorizationModule)], controllers: [BoardController, ColumnController, CardController, ElementController, BoardSubmissionController], providers: [BoardUc, BoardNodePermissionService, ColumnUc, CardUc, ElementUc, SubmissionItemUc, CourseRepo], }) diff --git a/apps/server/src/modules/board/controller/api-test/board-create.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-create-in-course.api.spec.ts similarity index 99% rename from apps/server/src/modules/board/controller/api-test/board-create.api.spec.ts rename to apps/server/src/modules/board/controller/api-test/board-create-in-course.api.spec.ts index 63e184fa898..1ca293aad55 100644 --- a/apps/server/src/modules/board/controller/api-test/board-create.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-create-in-course.api.spec.ts @@ -9,7 +9,7 @@ import { CreateBoardBodyParams } from '../dto'; const baseRouteName = '/boards'; -describe(`create board (api)`, () => { +describe(`create board in course (api)`, () => { let app: INestApplication; let em: EntityManager; let testApiClient: TestApiClient; diff --git a/apps/server/src/modules/board/controller/api-test/board-create-in-room.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-create-in-room.api.spec.ts new file mode 100644 index 00000000000..50d573674ef --- /dev/null +++ b/apps/server/src/modules/board/controller/api-test/board-create-in-room.api.spec.ts @@ -0,0 +1,294 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { ServerTestModule } from '@modules/server/server.module'; +import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Permission } from '@shared/domain/interface'; +import { RoleName } from '@shared/domain/interface/rolename.enum'; +import { cleanupCollections, groupEntityFactory, roleFactory, TestApiClient, userFactory } from '@shared/testing'; +import { accountFactory } from '@src/modules/account/testing'; +import { GroupEntityTypes } from '@src/modules/group/entity'; +import { roomMemberEntityFactory } from '@src/modules/room-member/testing'; +import { roomEntityFactory } from '@src/modules/room/testing'; +import { BoardExternalReferenceType, BoardLayout } from '../../domain'; +import { BoardNodeEntity } from '../../repo'; +import { CreateBoardBodyParams } from '../dto'; + +const baseRouteName = '/boards'; + +describe(`create board in room (api)`, () => { + let app: INestApplication; + let em: EntityManager; + let testApiClient: TestApiClient; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ServerTestModule], + }).compile(); + + app = module.createNestApplication(); + await app.init(); + em = module.get(EntityManager); + testApiClient = new TestApiClient(app, baseRouteName); + }); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(async () => { + await cleanupCollections(em); + }); + + describe('When request is valid', () => { + describe('When user is allowed to edit the room', () => { + const setup = async () => { + const user = userFactory.buildWithId(); + const account = accountFactory.withUser(user).build(); + + const role = roleFactory.buildWithId({ name: RoleName.ROOM_EDITOR, permissions: [Permission.ROOM_EDIT] }); + + const userGroup = groupEntityFactory.buildWithId({ + type: GroupEntityTypes.ROOM, + users: [{ user, role }], + }); + + const room = roomEntityFactory.buildWithId(); + + const roomMember = roomMemberEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + + await em.persistAndFlush([account, user, role, userGroup, room, roomMember]); + em.clear(); + + const loggedInClient = await testApiClient.login(account); + + return { loggedInClient, room }; + }; + + it('should return status 201 and board', async () => { + const { loggedInClient, room } = await setup(); + const title = 'new board'; + + const response = await loggedInClient.post(undefined, { + title, + parentId: room.id, + parentType: BoardExternalReferenceType.Room, + layout: BoardLayout.COLUMNS, + }); + + const boardId = (response.body as { id: string }).id; + expect(response.status).toEqual(201); + expect(boardId).toBeDefined(); + + const dbResult = await em.findOneOrFail(BoardNodeEntity, boardId); + expect(dbResult.title).toEqual(title); + }); + + describe('Board layout', () => { + describe(`When layout is set to "${BoardLayout.COLUMNS}"`, () => { + it('should create a column board', async () => { + const { loggedInClient, room } = await setup(); + + const response = await loggedInClient.post(undefined, { + title: 'new board', + parentId: room.id, + parentType: BoardExternalReferenceType.Room, + layout: BoardLayout.COLUMNS, + }); + + const boardId = (response.body as { id: string }).id; + expect(response.status).toEqual(201); + expect(boardId).toBeDefined(); + + const dbResult = await em.findOneOrFail(BoardNodeEntity, boardId); + expect(dbResult.layout).toEqual(BoardLayout.COLUMNS); + }); + }); + + describe(`When layout is set to "${BoardLayout.LIST}"`, () => { + it('should create a list board', async () => { + const { loggedInClient, room } = await setup(); + + const response = await loggedInClient.post(undefined, { + title: 'new board', + parentId: room.id, + parentType: BoardExternalReferenceType.Room, + layout: BoardLayout.LIST, + }); + + const boardId = (response.body as { id: string }).id; + expect(response.status).toEqual(201); + expect(boardId).toBeDefined(); + + const dbResult = await em.findOneOrFail(BoardNodeEntity, boardId); + expect(dbResult.layout).toEqual(BoardLayout.LIST); + }); + }); + }); + + describe('When layout is omitted', () => { + it('should return status 400', async () => { + const { loggedInClient, room } = await setup(); + + const response = await loggedInClient.post(undefined, >{ + title: 'new board', + parentId: room.id, + parentType: BoardExternalReferenceType.Room, + layout: undefined, + }); + + expect(response.status).toEqual(400); + }); + }); + + describe('When layout is invalid', () => { + it('should return status 400', async () => { + const { loggedInClient, room } = await setup(); + + const response = await loggedInClient.post(undefined, >{ + title: 'new board', + parentId: room.id, + parentType: BoardExternalReferenceType.Room, + layout: 'invalid', + }); + + expect(response.status).toEqual(400); + }); + }); + }); + + describe('When user is only allowed to view the room', () => { + const setup = async () => { + const user = userFactory.buildWithId(); + const account = accountFactory.withUser(user).build(); + + const role = roleFactory.buildWithId({ name: RoleName.ROOM_EDITOR, permissions: [Permission.ROOM_VIEW] }); + + const userGroup = groupEntityFactory.buildWithId({ + type: GroupEntityTypes.ROOM, + users: [{ user, role }], + }); + + const room = roomEntityFactory.buildWithId(); + + const roomMember = roomMemberEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + + await em.persistAndFlush([account, user, role, userGroup, room, roomMember]); + em.clear(); + + const loggedInClient = await testApiClient.login(account); + + return { loggedInClient, room }; + }; + + it('should return status 403', async () => { + const { loggedInClient, room } = await setup(); + + const response = await loggedInClient.post(undefined, { + title: 'new board', + parentId: room.id, + parentType: BoardExternalReferenceType.Room, + layout: BoardLayout.COLUMNS, + }); + + expect(response.status).toEqual(403); + }); + }); + + describe('When user is not allowed in the room at all', () => { + const setup = async () => { + const user = userFactory.buildWithId(); + const account = accountFactory.withUser(user).build(); + + const room = roomEntityFactory.buildWithId(); + + await em.persistAndFlush([account, user, room]); + em.clear(); + + const loggedInClient = await testApiClient.login(account); + + return { loggedInClient, room }; + }; + + it('should return status 403', async () => { + const { loggedInClient, room } = await setup(); + + const response = await loggedInClient.post(undefined, { + title: 'new board', + parentId: room.id, + parentType: BoardExternalReferenceType.Room, + layout: BoardLayout.COLUMNS, + }); + + expect(response.status).toEqual(403); + }); + }); + }); + + describe('When request is invalid', () => { + const setup = async () => { + const user = userFactory.buildWithId(); + const account = accountFactory.withUser(user).build(); + + const role = roleFactory.buildWithId({ name: RoleName.ROOM_EDITOR, permissions: [Permission.ROOM_EDIT] }); + + const userGroup = groupEntityFactory.buildWithId({ + type: GroupEntityTypes.ROOM, + users: [{ user, role }], + }); + + const room = roomEntityFactory.buildWithId(); + + const roomMember = roomMemberEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + + await em.persistAndFlush([account, user, role, userGroup, room, roomMember]); + em.clear(); + + const loggedInClient = await testApiClient.login(account); + + return { loggedInClient, room }; + }; + + describe('When title is empty', () => { + it('should return status 400', async () => { + const { loggedInClient, room } = await setup(); + + const response = await loggedInClient.post(undefined, { + title: '', + parentId: room.id, + parentType: BoardExternalReferenceType.Room, + }); + + expect(response.status).toEqual(400); + }); + }); + + describe('When title is too long', () => { + it('should return status 400', async () => { + const { loggedInClient, room } = await setup(); + + const response = await loggedInClient.post(undefined, { + title: 'a'.repeat(101), + parentId: room.id, + parentType: BoardExternalReferenceType.Room, + }); + + expect(response.status).toEqual(400); + }); + }); + + describe('When parent type is invalid', () => { + it('should return status 400', async () => { + const { loggedInClient, room } = await setup(); + + const response = await loggedInClient.post(undefined, >{ + title: 'new board', + parentId: room.id, + parentType: 'invalid', + layout: BoardLayout.COLUMNS, + }); + + expect(response.status).toEqual(400); + }); + }); + }); +}); diff --git a/apps/server/src/modules/board/domain/types/board-external-reference.ts b/apps/server/src/modules/board/domain/types/board-external-reference.ts index 5d9fd94a599..a195d6b1578 100644 --- a/apps/server/src/modules/board/domain/types/board-external-reference.ts +++ b/apps/server/src/modules/board/domain/types/board-external-reference.ts @@ -1,10 +1,11 @@ import type { EntityId } from '@shared/domain/types'; export enum BoardExternalReferenceType { - 'Course' = 'course', - 'User' = 'user', + Course = 'course', + Room = 'room', + User = 'user', // TODO - // 'ExternalTool' = 'external-tool', + // ExternalTool = 'external-tool', } export interface BoardExternalReference { diff --git a/apps/server/src/modules/board/uc/board.uc.ts b/apps/server/src/modules/board/uc/board.uc.ts index a48be369c1c..59552ff45d0 100644 --- a/apps/server/src/modules/board/uc/board.uc.ts +++ b/apps/server/src/modules/board/uc/board.uc.ts @@ -5,8 +5,9 @@ import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { CourseRepo } from '@shared/repo'; import { LegacyLogger } from '@src/core/logger'; +import { RoomMemberService } from '@src/modules/room-member'; import { CreateBoardBodyParams } from '../controller/dto'; -import { BoardExternalReference, BoardNodeFactory, Column, ColumnBoard } from '../domain'; +import { BoardExternalReference, BoardExternalReferenceType, BoardNodeFactory, Column, ColumnBoard } from '../domain'; import { BoardNodePermissionService, BoardNodeService, ColumnBoardService } from '../service'; @Injectable() @@ -15,6 +16,7 @@ export class BoardUc { @Inject(forwardRef(() => AuthorizationService)) // TODO is this needed? private readonly authorizationService: AuthorizationService, private readonly boardPermissionService: BoardNodePermissionService, + private readonly roomMemberService: RoomMemberService, private readonly boardNodeService: BoardNodeService, private readonly columnBoardService: ColumnBoardService, private readonly logger: LegacyLogger, @@ -27,13 +29,7 @@ export class BoardUc { async createBoard(userId: EntityId, params: CreateBoardBodyParams): Promise { this.logger.debug({ action: 'createBoard', userId, title: params.title }); - const user = await this.authorizationService.getUserWithPermissions(userId); - const course = await this.courseRepo.findById(params.parentId); - - this.authorizationService.checkPermission(user, course, { - action: Action.write, - requiredPermissions: [Permission.COURSE_EDIT], - }); + await this.checkParentWritePermission(userId, { type: params.parentType, id: params.parentId }); const board = this.boardNodeFactory.buildColumnBoard({ context: { type: params.parentType, id: params.parentId }, @@ -147,4 +143,24 @@ export class BoardUc { await this.boardNodeService.updateVisibility(board, isVisible); return board; } + + private async checkParentWritePermission(userId: EntityId, context: BoardExternalReference) { + const user = await this.authorizationService.getUserWithPermissions(userId); + + if (context.type === BoardExternalReferenceType.Course) { + const course = await this.courseRepo.findById(context.id); + + this.authorizationService.checkPermission(user, course, { + action: Action.write, + requiredPermissions: [Permission.COURSE_EDIT], + }); + } else if (context.type === BoardExternalReferenceType.Room) { + const roomMemberAuthorizable = await this.roomMemberService.getRoomMemberAuthorizable(context.id); + + this.authorizationService.checkPermission(user, roomMemberAuthorizable, { + action: Action.write, + requiredPermissions: [], + }); + } + } } From c9fa8b8ae4e22206e57cf4d07380cfb83fcddd61 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Mon, 28 Oct 2024 17:26:32 +0100 Subject: [PATCH 02/20] add api endpoint for getting room content --- .../response/room-content-item.response.ts | 30 +++ .../api/dto/response/room-content.response.ts | 13 ++ .../modules/room/api/mapper/room.mapper.ts | 23 +++ .../src/modules/room/api/room.controller.ts | 20 ++ apps/server/src/modules/room/api/room.uc.ts | 19 ++ .../api/test/room-get-content.api.spec.ts | 173 ++++++++++++++++++ .../src/modules/room/room-api.module.ts | 3 +- 7 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 apps/server/src/modules/room/api/dto/response/room-content-item.response.ts create mode 100644 apps/server/src/modules/room/api/dto/response/room-content.response.ts create mode 100644 apps/server/src/modules/room/api/test/room-get-content.api.spec.ts diff --git a/apps/server/src/modules/room/api/dto/response/room-content-item.response.ts b/apps/server/src/modules/room/api/dto/response/room-content-item.response.ts new file mode 100644 index 00000000000..ec8c210e865 --- /dev/null +++ b/apps/server/src/modules/room/api/dto/response/room-content-item.response.ts @@ -0,0 +1,30 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export enum RoomContentItemType { + COLUMN_BOARD = 'column-board', +} + +export class RoomContentItemResponse { + @ApiProperty({ enum: RoomContentItemType, enumName: 'RoomContentItemType' }) + type: RoomContentItemType; + + @ApiProperty() + id: string; + + @ApiProperty() + name: string; + + @ApiProperty({ type: Date }) + createdAt: Date; + + @ApiProperty({ type: Date }) + updatedAt: Date; + + constructor(item: RoomContentItemResponse) { + this.type = item.type; + this.id = item.id; + this.name = item.name; + this.createdAt = item.createdAt; + this.updatedAt = item.updatedAt; + } +} diff --git a/apps/server/src/modules/room/api/dto/response/room-content.response.ts b/apps/server/src/modules/room/api/dto/response/room-content.response.ts new file mode 100644 index 00000000000..9f2c8f9fb43 --- /dev/null +++ b/apps/server/src/modules/room/api/dto/response/room-content.response.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { PaginationResponse } from '@shared/controller'; +import { RoomContentItemResponse } from './room-content-item.response'; + +export class RoomContentResponse extends PaginationResponse { + constructor(data: RoomContentItemResponse[], total: number, skip?: number, limit?: number) { + super(total, skip, limit); + this.data = data; + } + + @ApiProperty({ type: [RoomContentItemResponse] }) + data: RoomContentItemResponse[]; +} diff --git a/apps/server/src/modules/room/api/mapper/room.mapper.ts b/apps/server/src/modules/room/api/mapper/room.mapper.ts index 3b559182926..e29f682b872 100644 --- a/apps/server/src/modules/room/api/mapper/room.mapper.ts +++ b/apps/server/src/modules/room/api/mapper/room.mapper.ts @@ -1,6 +1,9 @@ import { Page } from '@shared/domain/domainobject'; +import { ColumnBoard } from '@src/modules/board'; import { Room } from '../../domain/do/room.do'; import { RoomPaginationParams } from '../dto/request/room-pagination.params'; +import { RoomContentItemResponse, RoomContentItemType } from '../dto/response/room-content-item.response'; +import { RoomContentResponse } from '../dto/response/room-content.response'; import { RoomDetailsResponse } from '../dto/response/room-details.response'; import { RoomItemResponse } from '../dto/response/room-item.response'; import { RoomListResponse } from '../dto/response/room-list.response'; @@ -42,4 +45,24 @@ export class RoomMapper { return response; } + + static mapToRoomContentItemReponse(board: ColumnBoard): RoomContentItemResponse { + const response = new RoomContentItemResponse({ + type: RoomContentItemType.COLUMN_BOARD, + id: board.id, + name: board.title, + createdAt: board.createdAt, + updatedAt: board.updatedAt, + }); + + return response; + } + + static mapToRoomContentResponse(columnBoards: ColumnBoard[]): RoomContentResponse { + const itemData = columnBoards.map((board) => this.mapToRoomContentItemReponse(board)); + + const response = new RoomContentResponse(itemData, columnBoards.length); + + return response; + } } diff --git a/apps/server/src/modules/room/api/room.controller.ts b/apps/server/src/modules/room/api/room.controller.ts index 6847a853b00..6d11e8acfd3 100644 --- a/apps/server/src/modules/room/api/room.controller.ts +++ b/apps/server/src/modules/room/api/room.controller.ts @@ -28,6 +28,7 @@ import { RoomListResponse } from './dto/response/room-list.response'; import { RoomMapper } from './mapper/room.mapper'; import { RoomUc } from './room.uc'; import { RoomItemResponse } from './dto/response/room-item.response'; +import { RoomContentResponse } from './dto/response/room-content.response'; @ApiTags('Room') @JwtAuthentication() @@ -92,6 +93,25 @@ export class RoomController { return response; } + @Get(':roomId/content') + @ApiOperation({ summary: 'Get the content of a room' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Returns the content of a room', type: RoomContentResponse }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, type: ApiValidationError }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, type: UnauthorizedException }) + @ApiResponse({ status: HttpStatus.FORBIDDEN, type: ForbiddenException }) + @ApiResponse({ status: HttpStatus.NOT_FOUND, type: NotFoundException }) + @ApiResponse({ status: '5XX', type: ErrorResponse }) + async getRoomContent( + @CurrentUser() currentUser: ICurrentUser, + @Param() urlParams: RoomUrlParams + ): Promise { + const boards = await this.roomUc.getRoomContent(currentUser.userId, urlParams.roomId); + + const response = RoomMapper.mapToRoomContentResponse(boards); + + return response; + } + @Patch(':roomId') @ApiOperation({ summary: 'Create a new room' }) @ApiResponse({ status: HttpStatus.OK, description: 'Returns the details of a room', type: RoomDetailsResponse }) diff --git a/apps/server/src/modules/room/api/room.uc.ts b/apps/server/src/modules/room/api/room.uc.ts index 16c22c14fd5..affcd32e697 100644 --- a/apps/server/src/modules/room/api/room.uc.ts +++ b/apps/server/src/modules/room/api/room.uc.ts @@ -5,6 +5,7 @@ import { Page } from '@shared/domain/domainobject'; import { IFindOptions, Permission, RoleName } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { Action, AuthorizationService } from '@src/modules/authorization'; +import { BoardExternalReferenceType, ColumnBoard, ColumnBoardService } from '@src/modules/board'; import { RoomMemberService } from '@src/modules/room-member'; import { Room, RoomCreateProps, RoomService, RoomUpdateProps } from '../domain'; import { RoomConfig } from '../room.config'; @@ -15,6 +16,7 @@ export class RoomUc { private readonly configService: ConfigService, private readonly roomService: RoomService, private readonly roomMemberService: RoomMemberService, + private readonly columnBoardService: ColumnBoardService, private readonly authorizationService: AuthorizationService ) {} @@ -50,6 +52,23 @@ export class RoomUc { return room; } + public async getRoomContent(userId: EntityId, roomId: EntityId): Promise { + this.checkFeatureEnabled(); + + await this.roomService.getSingleRoom(roomId); + await this.checkRoomAuthorization(userId, roomId, Action.read); + + const boards = await this.columnBoardService.findByExternalReference( + { + type: BoardExternalReferenceType.Room, + id: roomId, + }, + 0 + ); + + return boards; + } + public async updateRoom(userId: EntityId, roomId: EntityId, props: RoomUpdateProps): Promise { this.checkFeatureEnabled(); const room = await this.roomService.getSingleRoom(roomId); diff --git a/apps/server/src/modules/room/api/test/room-get-content.api.spec.ts b/apps/server/src/modules/room/api/test/room-get-content.api.spec.ts new file mode 100644 index 00000000000..649b3c7f2c4 --- /dev/null +++ b/apps/server/src/modules/room/api/test/room-get-content.api.spec.ts @@ -0,0 +1,173 @@ +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { Permission, RoleName } from '@shared/domain/interface'; +import { + cleanupCollections, + groupEntityFactory, + roleFactory, + TestApiClient, + UserAndAccountTestFactory, +} from '@shared/testing'; +import { BoardExternalReferenceType } from '@src/modules/board'; +import { columnBoardEntityFactory } from '@src/modules/board/testing'; +import { GroupEntityTypes } from '@src/modules/group/entity'; +import { roomMemberEntityFactory } from '@src/modules/room-member/testing'; +import { serverConfig, ServerConfig, ServerTestModule } from '@src/modules/server'; +import { roomEntityFactory } from '../../testing'; +import { RoomContentItemType } from '../dto/response/room-content-item.response'; + +describe('Room Controller (API)', () => { + let app: INestApplication; + let em: EntityManager; + let testApiClient: TestApiClient; + let config: ServerConfig; + + beforeAll(async () => { + const moduleFixture = await Test.createTestingModule({ + imports: [ServerTestModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + em = app.get(EntityManager); + testApiClient = new TestApiClient(app, 'rooms'); + + config = serverConfig(); + }); + + beforeEach(async () => { + await cleanupCollections(em); + config.FEATURE_ROOMS_ENABLED = true; + }); + + afterAll(async () => { + await app.close(); + }); + + describe('GET /rooms/:id/content', () => { + describe('when the user is not authenticated', () => { + it('should return a 401 error', async () => { + const someId = new ObjectId().toHexString(); + const response = await testApiClient.get(someId); + expect(response.status).toBe(HttpStatus.UNAUTHORIZED); + }); + }); + + describe('when the feature is disabled', () => { + const setup = async () => { + config.FEATURE_ROOMS_ENABLED = false; + + const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); + await em.persistAndFlush([studentAccount, studentUser]); + em.clear(); + + const loggedInClient = await testApiClient.login(studentAccount); + + return { loggedInClient }; + }; + + it('should return a 403 error', async () => { + const { loggedInClient } = await setup(); + const someId = new ObjectId().toHexString(); + const response = await loggedInClient.get(`${someId}/content`); + expect(response.status).toBe(HttpStatus.FORBIDDEN); + }); + }); + + describe('when id is not a valid mongo id', () => { + const setup = async () => { + const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); + await em.persistAndFlush([studentAccount, studentUser]); + em.clear(); + + const loggedInClient = await testApiClient.login(studentAccount); + + return { loggedInClient }; + }; + + it('should return a 400 error', async () => { + const { loggedInClient } = await setup(); + const response = await loggedInClient.get('42/content'); + expect(response.status).toBe(HttpStatus.BAD_REQUEST); + }); + }); + + describe('when the user has the required permissions', () => { + const setup = async () => { + const room = roomEntityFactory.build(); + const board = columnBoardEntityFactory.build({ + context: { type: BoardExternalReferenceType.Room, id: room.id }, + }); + const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); + const role = roleFactory.buildWithId({ + name: RoleName.ROOM_VIEWER, + permissions: [Permission.ROOM_VIEW], + }); + const userGroupEntity = groupEntityFactory.buildWithId({ + type: GroupEntityTypes.ROOM, + users: [{ role, user: studentUser }], + organization: studentUser.school, + externalSource: undefined, + }); + const roomMember = roomMemberEntityFactory.build({ userGroupId: userGroupEntity.id, roomId: room.id }); + await em.persistAndFlush([room, board, studentAccount, studentUser, role, userGroupEntity, roomMember]); + em.clear(); + + const loggedInClient = await testApiClient.login(studentAccount); + + return { loggedInClient, room, board }; + }; + + describe('when the room exists', () => { + it('should return a room', async () => { + const { loggedInClient, room, board } = await setup(); + + const response = await loggedInClient.get(`${room.id}/content`); + expect(response.status).toBe(HttpStatus.OK); + expect((response.body as { data: Record }).data[0]).toEqual({ + type: RoomContentItemType.COLUMN_BOARD, + id: board.id, + name: board.title, + createdAt: board.createdAt.toISOString(), + updatedAt: board.updatedAt.toISOString(), + }); + }); + }); + + describe('when the room does not exist', () => { + it('should return a 404 error', async () => { + const { loggedInClient } = await setup(); + const someId = new ObjectId().toHexString(); + + const response = await loggedInClient.get(someId); + + expect(response.status).toBe(HttpStatus.NOT_FOUND); + }); + }); + }); + + describe('when the user has not the required permissions', () => { + const setup = async () => { + const room = roomEntityFactory.build(); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); + await em.persistAndFlush([room, teacherAccount, teacherUser]); + em.clear(); + + const loggedInClient = await testApiClient.login(teacherAccount); + + return { loggedInClient, room }; + }; + + describe('when the room exists', () => { + it('should return 403', async () => { + const { loggedInClient, room } = await setup(); + + const response = await loggedInClient.get(room.id); + + expect(response.status).toBe(HttpStatus.FORBIDDEN); + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/room/room-api.module.ts b/apps/server/src/modules/room/room-api.module.ts index 5af3626655a..1e495606063 100644 --- a/apps/server/src/modules/room/room-api.module.ts +++ b/apps/server/src/modules/room/room-api.module.ts @@ -4,9 +4,10 @@ import { LoggerModule } from '@src/core/logger'; import { RoomMemberModule } from '../room-member/room-member.module'; import { RoomController, RoomUc } from './api'; import { RoomModule } from './room.module'; +import { BoardModule } from '../board'; @Module({ - imports: [RoomModule, AuthorizationModule, LoggerModule, RoomMemberModule], + imports: [RoomModule, AuthorizationModule, LoggerModule, RoomMemberModule, BoardModule], controllers: [RoomController], providers: [RoomUc], }) From e3fac0a8e6d3c5d61af0c9a404d1f5362087f941 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Tue, 29 Oct 2024 07:53:20 +0100 Subject: [PATCH 03/20] refactor api from content to boards --- .../dto/response/room-board-item.response.ts | 22 ++++++++++++++ .../dto/response/room-board-list.response.ts | 13 ++++++++ .../response/room-content-item.response.ts | 30 ------------------- .../api/dto/response/room-content.response.ts | 13 -------- .../modules/room/api/mapper/room.mapper.ts | 17 +++++------ .../src/modules/room/api/room.controller.ts | 16 +++++----- apps/server/src/modules/room/api/room.uc.ts | 2 +- .../api/test/room-get-content.api.spec.ts | 12 ++++---- 8 files changed, 57 insertions(+), 68 deletions(-) create mode 100644 apps/server/src/modules/room/api/dto/response/room-board-item.response.ts create mode 100644 apps/server/src/modules/room/api/dto/response/room-board-list.response.ts delete mode 100644 apps/server/src/modules/room/api/dto/response/room-content-item.response.ts delete mode 100644 apps/server/src/modules/room/api/dto/response/room-content.response.ts diff --git a/apps/server/src/modules/room/api/dto/response/room-board-item.response.ts b/apps/server/src/modules/room/api/dto/response/room-board-item.response.ts new file mode 100644 index 00000000000..f20b5f77bea --- /dev/null +++ b/apps/server/src/modules/room/api/dto/response/room-board-item.response.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class RoomBoardItemResponse { + @ApiProperty() + id: string; + + @ApiProperty() + title: string; + + @ApiProperty({ type: Date }) + createdAt: Date; + + @ApiProperty({ type: Date }) + updatedAt: Date; + + constructor(item: RoomBoardItemResponse) { + this.id = item.id; + this.title = item.title; + this.createdAt = item.createdAt; + this.updatedAt = item.updatedAt; + } +} diff --git a/apps/server/src/modules/room/api/dto/response/room-board-list.response.ts b/apps/server/src/modules/room/api/dto/response/room-board-list.response.ts new file mode 100644 index 00000000000..60d8510709b --- /dev/null +++ b/apps/server/src/modules/room/api/dto/response/room-board-list.response.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { PaginationResponse } from '@shared/controller'; +import { RoomBoardItemResponse } from './room-board-item.response'; + +export class RoomBoardListResponse extends PaginationResponse { + constructor(data: RoomBoardItemResponse[], total: number, skip?: number, limit?: number) { + super(total, skip, limit); + this.data = data; + } + + @ApiProperty({ type: [RoomBoardItemResponse] }) + data: RoomBoardItemResponse[]; +} diff --git a/apps/server/src/modules/room/api/dto/response/room-content-item.response.ts b/apps/server/src/modules/room/api/dto/response/room-content-item.response.ts deleted file mode 100644 index ec8c210e865..00000000000 --- a/apps/server/src/modules/room/api/dto/response/room-content-item.response.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export enum RoomContentItemType { - COLUMN_BOARD = 'column-board', -} - -export class RoomContentItemResponse { - @ApiProperty({ enum: RoomContentItemType, enumName: 'RoomContentItemType' }) - type: RoomContentItemType; - - @ApiProperty() - id: string; - - @ApiProperty() - name: string; - - @ApiProperty({ type: Date }) - createdAt: Date; - - @ApiProperty({ type: Date }) - updatedAt: Date; - - constructor(item: RoomContentItemResponse) { - this.type = item.type; - this.id = item.id; - this.name = item.name; - this.createdAt = item.createdAt; - this.updatedAt = item.updatedAt; - } -} diff --git a/apps/server/src/modules/room/api/dto/response/room-content.response.ts b/apps/server/src/modules/room/api/dto/response/room-content.response.ts deleted file mode 100644 index 9f2c8f9fb43..00000000000 --- a/apps/server/src/modules/room/api/dto/response/room-content.response.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { PaginationResponse } from '@shared/controller'; -import { RoomContentItemResponse } from './room-content-item.response'; - -export class RoomContentResponse extends PaginationResponse { - constructor(data: RoomContentItemResponse[], total: number, skip?: number, limit?: number) { - super(total, skip, limit); - this.data = data; - } - - @ApiProperty({ type: [RoomContentItemResponse] }) - data: RoomContentItemResponse[]; -} diff --git a/apps/server/src/modules/room/api/mapper/room.mapper.ts b/apps/server/src/modules/room/api/mapper/room.mapper.ts index e29f682b872..ad7efaef26b 100644 --- a/apps/server/src/modules/room/api/mapper/room.mapper.ts +++ b/apps/server/src/modules/room/api/mapper/room.mapper.ts @@ -2,8 +2,8 @@ import { Page } from '@shared/domain/domainobject'; import { ColumnBoard } from '@src/modules/board'; import { Room } from '../../domain/do/room.do'; import { RoomPaginationParams } from '../dto/request/room-pagination.params'; -import { RoomContentItemResponse, RoomContentItemType } from '../dto/response/room-content-item.response'; -import { RoomContentResponse } from '../dto/response/room-content.response'; +import { RoomBoardItemResponse } from '../dto/response/room-board-item.response'; +import { RoomBoardListResponse } from '../dto/response/room-board-list.response'; import { RoomDetailsResponse } from '../dto/response/room-details.response'; import { RoomItemResponse } from '../dto/response/room-item.response'; import { RoomListResponse } from '../dto/response/room-list.response'; @@ -46,11 +46,10 @@ export class RoomMapper { return response; } - static mapToRoomContentItemReponse(board: ColumnBoard): RoomContentItemResponse { - const response = new RoomContentItemResponse({ - type: RoomContentItemType.COLUMN_BOARD, + static mapToRoomBoardItemReponse(board: ColumnBoard): RoomBoardItemResponse { + const response = new RoomBoardItemResponse({ id: board.id, - name: board.title, + title: board.title, createdAt: board.createdAt, updatedAt: board.updatedAt, }); @@ -58,10 +57,10 @@ export class RoomMapper { return response; } - static mapToRoomContentResponse(columnBoards: ColumnBoard[]): RoomContentResponse { - const itemData = columnBoards.map((board) => this.mapToRoomContentItemReponse(board)); + static mapToRoomBoardListResponse(columnBoards: ColumnBoard[]): RoomBoardListResponse { + const itemData = columnBoards.map((board) => this.mapToRoomBoardItemReponse(board)); - const response = new RoomContentResponse(itemData, columnBoards.length); + const response = new RoomBoardListResponse(itemData, columnBoards.length); return response; } diff --git a/apps/server/src/modules/room/api/room.controller.ts b/apps/server/src/modules/room/api/room.controller.ts index 6d11e8acfd3..66bfbcb92e5 100644 --- a/apps/server/src/modules/room/api/room.controller.ts +++ b/apps/server/src/modules/room/api/room.controller.ts @@ -28,7 +28,7 @@ import { RoomListResponse } from './dto/response/room-list.response'; import { RoomMapper } from './mapper/room.mapper'; import { RoomUc } from './room.uc'; import { RoomItemResponse } from './dto/response/room-item.response'; -import { RoomContentResponse } from './dto/response/room-content.response'; +import { RoomBoardListResponse } from './dto/response/room-board-list.response'; @ApiTags('Room') @JwtAuthentication() @@ -93,21 +93,21 @@ export class RoomController { return response; } - @Get(':roomId/content') - @ApiOperation({ summary: 'Get the content of a room' }) - @ApiResponse({ status: HttpStatus.OK, description: 'Returns the content of a room', type: RoomContentResponse }) + @Get(':roomId/boards') + @ApiOperation({ summary: 'Get the boards of a room' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Returns the boards of a room', type: RoomBoardListResponse }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, type: ApiValidationError }) @ApiResponse({ status: HttpStatus.UNAUTHORIZED, type: UnauthorizedException }) @ApiResponse({ status: HttpStatus.FORBIDDEN, type: ForbiddenException }) @ApiResponse({ status: HttpStatus.NOT_FOUND, type: NotFoundException }) @ApiResponse({ status: '5XX', type: ErrorResponse }) - async getRoomContent( + async getRoomBoards( @CurrentUser() currentUser: ICurrentUser, @Param() urlParams: RoomUrlParams - ): Promise { - const boards = await this.roomUc.getRoomContent(currentUser.userId, urlParams.roomId); + ): Promise { + const boards = await this.roomUc.getRoomBoards(currentUser.userId, urlParams.roomId); - const response = RoomMapper.mapToRoomContentResponse(boards); + const response = RoomMapper.mapToRoomBoardListResponse(boards); return response; } diff --git a/apps/server/src/modules/room/api/room.uc.ts b/apps/server/src/modules/room/api/room.uc.ts index affcd32e697..9e2787975e8 100644 --- a/apps/server/src/modules/room/api/room.uc.ts +++ b/apps/server/src/modules/room/api/room.uc.ts @@ -52,7 +52,7 @@ export class RoomUc { return room; } - public async getRoomContent(userId: EntityId, roomId: EntityId): Promise { + public async getRoomBoards(userId: EntityId, roomId: EntityId): Promise { this.checkFeatureEnabled(); await this.roomService.getSingleRoom(roomId); diff --git a/apps/server/src/modules/room/api/test/room-get-content.api.spec.ts b/apps/server/src/modules/room/api/test/room-get-content.api.spec.ts index 649b3c7f2c4..17251b879a7 100644 --- a/apps/server/src/modules/room/api/test/room-get-content.api.spec.ts +++ b/apps/server/src/modules/room/api/test/room-get-content.api.spec.ts @@ -15,7 +15,6 @@ import { GroupEntityTypes } from '@src/modules/group/entity'; import { roomMemberEntityFactory } from '@src/modules/room-member/testing'; import { serverConfig, ServerConfig, ServerTestModule } from '@src/modules/server'; import { roomEntityFactory } from '../../testing'; -import { RoomContentItemType } from '../dto/response/room-content-item.response'; describe('Room Controller (API)', () => { let app: INestApplication; @@ -45,7 +44,7 @@ describe('Room Controller (API)', () => { await app.close(); }); - describe('GET /rooms/:id/content', () => { + describe('GET /rooms/:id/boards', () => { describe('when the user is not authenticated', () => { it('should return a 401 error', async () => { const someId = new ObjectId().toHexString(); @@ -70,7 +69,7 @@ describe('Room Controller (API)', () => { it('should return a 403 error', async () => { const { loggedInClient } = await setup(); const someId = new ObjectId().toHexString(); - const response = await loggedInClient.get(`${someId}/content`); + const response = await loggedInClient.get(`${someId}/boards`); expect(response.status).toBe(HttpStatus.FORBIDDEN); }); }); @@ -88,7 +87,7 @@ describe('Room Controller (API)', () => { it('should return a 400 error', async () => { const { loggedInClient } = await setup(); - const response = await loggedInClient.get('42/content'); + const response = await loggedInClient.get('42/boards'); expect(response.status).toBe(HttpStatus.BAD_REQUEST); }); }); @@ -123,12 +122,11 @@ describe('Room Controller (API)', () => { it('should return a room', async () => { const { loggedInClient, room, board } = await setup(); - const response = await loggedInClient.get(`${room.id}/content`); + const response = await loggedInClient.get(`${room.id}/boards`); expect(response.status).toBe(HttpStatus.OK); expect((response.body as { data: Record }).data[0]).toEqual({ - type: RoomContentItemType.COLUMN_BOARD, id: board.id, - name: board.title, + title: board.title, createdAt: board.createdAt.toISOString(), updatedAt: board.updatedAt.toISOString(), }); From 9bd8bb3d5b543322b9cd98f46e0ff5dc2d8ddb04 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Tue, 29 Oct 2024 14:27:49 +0100 Subject: [PATCH 04/20] refactor static method --- .../modules/room-member/service/room-member.service.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/server/src/modules/room-member/service/room-member.service.ts b/apps/server/src/modules/room-member/service/room-member.service.ts index ae3d44fcfe4..c31dff3643d 100644 --- a/apps/server/src/modules/room-member/service/room-member.service.ts +++ b/apps/server/src/modules/room-member/service/room-member.service.ts @@ -38,11 +38,7 @@ export class RoomMemberService { return roomMember; } - private static buildRoomMemberAuthorizable( - roomId: EntityId, - group: Group, - roleSet: RoleDto[] - ): RoomMemberAuthorizable { + private buildRoomMemberAuthorizable(roomId: EntityId, group: Group, roleSet: RoleDto[]): RoomMemberAuthorizable { const members = group.users.map((groupUser): UserWithRoomRoles => { const roleDto = roleSet.find((role) => role.id === groupUser.roleId); if (roleDto === undefined) throw new BadRequestException('Role not found'); @@ -92,7 +88,7 @@ export class RoomMemberService { .map((item) => { const group = groupPage.data.find((g) => g.id === item.userGroupId); if (!group) return null; - return RoomMemberService.buildRoomMemberAuthorizable(item.roomId, group, roleSet); + return this.buildRoomMemberAuthorizable(item.roomId, group, roleSet); }) .filter((item): item is RoomMemberAuthorizable => item !== null); From 516f5dafdfe1cf68df67ad49accc7546defda249 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Tue, 29 Oct 2024 14:28:11 +0100 Subject: [PATCH 05/20] remove outdated TODO.md --- TODO.md | 91 --------------------------------------------------------- 1 file changed, 91 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 7fa92508759..00000000000 --- a/TODO.md +++ /dev/null @@ -1,91 +0,0 @@ -# Technical TODO around Nest Introduction - -## SUGGESTED - -- filter logs by request with reflect-metadata (see mikroorm em setup) -- disable Document from window -- find a name for base entity id type -- find a name for base entity class -- decide if we want to use our entity id type in all layers (also in dtos etc.) -- use index.ts files to bundle exports - we could use path names for imports then, e.g. @shared/domain -- check how we can implement mandatory/optional fields in dtos -- should we use Expose() as default in dtos? -- in the controller we have to prohibit serialization of properties that have no @EXPOSE -- find the best way ORM entity discovery -- decide where to put domain interfaces (directory) -- how can we log validation errors during development? -- sanitizer -- remove non-node async library -- fix async cleanup & remove timeout in tests -- test object creator for nest entities -- enable log only for failed tests: https://stackoverflow.com/a/61909588 -- remove mongoose history (keep one) -- remove custom npm packages (ldap, ...) -- API default tests to extend: auth required, fails without/succeeds with - -## ACCEPTED - -- documentation - - entity constructor - - em to be used in repositories only (!!!) - -- load/perf test - -- disable legacy ts support (app, tests) - -- fix .env/config for windows - -## MERGE - -- api path prefix cleanup: remove middleware and multiple path mounts, sync with nest -- user module stucture -- single domain: shared entity (main.ts), shared repository -- request.user.user in jwt strategy -- remove outdated sorting.ts -- remove default launch/settings json files, apply them -- fix https://github.com/hpi-schul-cloud/schulcloud-server/pull/2729#pullrequestreview-699615164 - - -## SELECTED - -- test shared / core module -- async test fixes (remove this.timeout and red promise chains) - -- db configuration - - - keep mongoose options as mongo options - - povider for mikroorm options and db url - - test db provider - - entity discovery - - check indexes in mikroorm: when are they updated? - - teardown (test, server module, main.ts) - - replikaset for test module - - entity discovery - -- news - - - uc cleanup: 2auth, visibilities - - document best practices/layers/orm - -- context: user-/request-context (see mikroorm/asynclocalstorage) - - -## DONE - -- check build & start for production with ops -- fix jest, linter, ... -- inject APP_FILTER (exception handler) and APP_INTERCEPTOR (logger), see core module -- custom error handling (log/response), see global-error.filter.ts -- watch docs should hot reload on md file change -- 404 error handling in feathers has to be replaced (tests too). better: have nest before feathers... but seems not to be working -- remove mongoose -- publish documentation, see https://hpi-schul-cloud.github.io/schulcloud-server/overview.html -- fix all tests (nest/legacy) -- remove legacy scripts from package json (except tests) goal: have separated tests (legacy/nest) but only execute the nest app -- using legacy database connection string -- v3 with/-out slash: diffenrent routes should respond with different result (/v3 is a resssource, /v3/ === /v3/index) -- vscode/lauch files: we put only default files into the repo -- naming of dtos and dto-files: api vs domain, we leave out "dto" suffix for simplicity (we know that they are dtos) and instead append a specific suffix: - e.g. - api: , , - domain: , From f8b8b95a90eae439530ab9f95bf256911d110683 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Tue, 29 Oct 2024 14:28:30 +0100 Subject: [PATCH 06/20] update roles seeds --- backup/setup/roles.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/backup/setup/roles.json b/backup/setup/roles.json index 01fc0562cce..243ac2ca7ad 100644 --- a/backup/setup/roles.json +++ b/backup/setup/roles.json @@ -581,5 +581,24 @@ ], "permissions": [], "__v": 0 + }, + { + "_id": { + "$oid": "6720b8621b61c9dd7ebd193b" + }, + "name": "room_viewer", + "permissions": [ + "ROOM_VIEW" + ] + }, + { + "_id": { + "$oid": "6720b8621b61c9dd7ebd193c" + }, + "name": "room_editor", + "permissions": [ + "ROOM_VIEW", + "ROOM_EDIT" + ] } ] From b068231028c1ca1b1096050d7c348e2470e61f6f Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Wed, 30 Oct 2024 15:16:03 +0100 Subject: [PATCH 07/20] add room context to board context service --- .../service/internal/board-context.service.ts | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/apps/server/src/modules/board/service/internal/board-context.service.ts b/apps/server/src/modules/board/service/internal/board-context.service.ts index 42725a45c36..99f5c346cad 100644 --- a/apps/server/src/modules/board/service/internal/board-context.service.ts +++ b/apps/server/src/modules/board/service/internal/board-context.service.ts @@ -1,11 +1,14 @@ import { Injectable } from '@nestjs/common'; +import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { CourseRepo } from '@shared/repo'; +import { RoomMemberService } from '@src/modules/room-member'; +import { UserWithRoomRoles } from '@src/modules/room-member/do/room-member-authorizable.do'; import { AnyBoardNode, BoardExternalReferenceType, BoardRoles, UserWithBoardRoles } from '../../domain'; @Injectable() export class BoardContextService { - constructor(private readonly courseRepo: CourseRepo) {} + constructor(private readonly courseRepo: CourseRepo, private readonly roomMemberService: RoomMemberService) {} async getUsersWithBoardRoles(rootNode: AnyBoardNode): Promise { if (!('context' in rootNode)) { @@ -14,9 +17,11 @@ export class BoardContextService { let usersWithRoles: UserWithBoardRoles[] = []; - if (rootNode.context.type === BoardExternalReferenceType.Course) + if (rootNode.context.type === BoardExternalReferenceType.Room) { + usersWithRoles = await this.getFromRoom(rootNode.context.id); + } else if (rootNode.context.type === BoardExternalReferenceType.Course) { usersWithRoles = await this.getFromCourse(rootNode.context.id); - else if (rootNode.context.type === BoardExternalReferenceType.User) { + } else if (rootNode.context.type === BoardExternalReferenceType.User) { usersWithRoles = this.getFromUser(rootNode.context.id); } else { throw new Error(`Unknown context type: '${rootNode.context.type as string}'`); @@ -25,6 +30,18 @@ export class BoardContextService { return usersWithRoles; } + private async getFromRoom(roomId: EntityId): Promise { + const roomMemberAuthorizable = await this.roomMemberService.getRoomMemberAuthorizable(roomId); + const usersWithRoles: UserWithBoardRoles[] = roomMemberAuthorizable.members.map((member) => { + const roles = this.getBoardRolesFromRoomMember(member); + return { + userId: member.userId, + roles, + }; + }); + return usersWithRoles; + } + private async getFromCourse(courseId: EntityId): Promise { const course = await this.courseRepo.findById(courseId); const usersWithRoles: UserWithBoardRoles[] = [ @@ -67,4 +84,17 @@ export class BoardContextService { return usersWithRoles; } + + private getBoardRolesFromRoomMember(member: UserWithRoomRoles): BoardRoles[] { + const isReader = member.roles.flatMap((role) => role.permissions ?? []).includes(Permission.ROOM_VIEW); + const isEditor = member.roles.flatMap((role) => role.permissions ?? []).includes(Permission.ROOM_EDIT); + + if (isEditor) { + return [BoardRoles.EDITOR]; + } + if (isReader) { + return [BoardRoles.READER]; + } + return []; + } } From c8cd5dfb10c6f7bbd5997aad896f1b0affae22c7 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Wed, 30 Oct 2024 15:20:45 +0100 Subject: [PATCH 08/20] add missing module import --- apps/server/src/modules/board/board.module.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/server/src/modules/board/board.module.ts b/apps/server/src/modules/board/board.module.ts index 86f8e2671b3..48a6296959c 100644 --- a/apps/server/src/modules/board/board.module.ts +++ b/apps/server/src/modules/board/board.module.ts @@ -9,6 +9,9 @@ import { Module } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; import { CourseRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; +import { AuthorizationModule } from '../authorization'; +import { RoomMemberModule } from '../room-member'; +import { BoardNodeRule } from './authorisation/board-node.rule'; import { BoardNodeFactory } from './domain'; import { BoardNodeRepo } from './repo'; import { @@ -30,8 +33,6 @@ import { ColumnBoardTitleService, ContentElementUpdateService, } from './service/internal'; -import { BoardNodeRule } from './authorisation/board-node.rule'; -import { AuthorizationModule } from '../authorization'; @Module({ imports: [ @@ -45,6 +46,7 @@ import { AuthorizationModule } from '../authorization'; CqrsModule, CollaborativeTextEditorModule, AuthorizationModule, + RoomMemberModule, ], providers: [ // TODO: move BoardDoAuthorizableService, BoardDoRepo, BoardDoService, BoardNodeRepo in separate module and move mediaboard related services in mediaboard module From 0a81cc075cdd62f642588efbbcb0993fb4ee3bc6 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Wed, 30 Oct 2024 15:27:37 +0100 Subject: [PATCH 09/20] add board layout reponse property --- .../dto/single-column-board/board-column-board.response.ts | 2 +- .../room/api/dto/response/room-board-item.response.ts | 5 +++++ apps/server/src/modules/room/api/mapper/room.mapper.ts | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-column-board.response.ts b/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-column-board.response.ts index a27ad9cc356..7db20a2424d 100644 --- a/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-column-board.response.ts +++ b/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-column-board.response.ts @@ -32,6 +32,6 @@ export class BoardColumnBoardResponse { @ApiProperty() columnBoardId: string; - @ApiProperty() + @ApiProperty({ enum: BoardLayout }) layout: BoardLayout; } diff --git a/apps/server/src/modules/room/api/dto/response/room-board-item.response.ts b/apps/server/src/modules/room/api/dto/response/room-board-item.response.ts index f20b5f77bea..8320c997f29 100644 --- a/apps/server/src/modules/room/api/dto/response/room-board-item.response.ts +++ b/apps/server/src/modules/room/api/dto/response/room-board-item.response.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; +import { BoardLayout } from '@src/modules/board'; export class RoomBoardItemResponse { @ApiProperty() @@ -7,6 +8,9 @@ export class RoomBoardItemResponse { @ApiProperty() title: string; + @ApiProperty({ enum: BoardLayout }) + layout: BoardLayout; + @ApiProperty({ type: Date }) createdAt: Date; @@ -16,6 +20,7 @@ export class RoomBoardItemResponse { constructor(item: RoomBoardItemResponse) { this.id = item.id; this.title = item.title; + this.layout = item.layout; this.createdAt = item.createdAt; this.updatedAt = item.updatedAt; } diff --git a/apps/server/src/modules/room/api/mapper/room.mapper.ts b/apps/server/src/modules/room/api/mapper/room.mapper.ts index ad7efaef26b..dad650f6c1e 100644 --- a/apps/server/src/modules/room/api/mapper/room.mapper.ts +++ b/apps/server/src/modules/room/api/mapper/room.mapper.ts @@ -50,6 +50,7 @@ export class RoomMapper { const response = new RoomBoardItemResponse({ id: board.id, title: board.title, + layout: board.layout, createdAt: board.createdAt, updatedAt: board.updatedAt, }); From 56b44a7791ef56127fba9eddf690d0a3aafac7fb Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Fri, 1 Nov 2024 10:22:09 +0100 Subject: [PATCH 10/20] update board layout api property --- .../dto/single-column-board/board-column-board.response.ts | 2 +- .../modules/room/api/dto/response/room-board-item.response.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-column-board.response.ts b/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-column-board.response.ts index 7db20a2424d..3a18b5f1a8a 100644 --- a/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-column-board.response.ts +++ b/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-column-board.response.ts @@ -32,6 +32,6 @@ export class BoardColumnBoardResponse { @ApiProperty() columnBoardId: string; - @ApiProperty({ enum: BoardLayout }) + @ApiProperty({ enum: BoardLayout, enumName: 'BoardLayout' }) layout: BoardLayout; } diff --git a/apps/server/src/modules/room/api/dto/response/room-board-item.response.ts b/apps/server/src/modules/room/api/dto/response/room-board-item.response.ts index 8320c997f29..e6908aa884a 100644 --- a/apps/server/src/modules/room/api/dto/response/room-board-item.response.ts +++ b/apps/server/src/modules/room/api/dto/response/room-board-item.response.ts @@ -8,7 +8,7 @@ export class RoomBoardItemResponse { @ApiProperty() title: string; - @ApiProperty({ enum: BoardLayout }) + @ApiProperty({ enum: BoardLayout, enumName: 'BoardLayout' }) layout: BoardLayout; @ApiProperty({ type: Date }) From 5bfc8889737012639b229fb7acb1c9da17fdfbcd Mon Sep 17 00:00:00 2001 From: MartinSchuhmacher Date: Fri, 1 Nov 2024 12:44:15 +0100 Subject: [PATCH 11/20] adding board context service tests for room context --- .../internal/board-context.service.spec.ts | 119 +++++++++++++++++- 1 file changed, 118 insertions(+), 1 deletion(-) diff --git a/apps/server/src/modules/board/service/internal/board-context.service.spec.ts b/apps/server/src/modules/board/service/internal/board-context.service.spec.ts index 46e09f3598e..a3e72571378 100644 --- a/apps/server/src/modules/board/service/internal/board-context.service.spec.ts +++ b/apps/server/src/modules/board/service/internal/board-context.service.spec.ts @@ -2,7 +2,12 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { CourseRepo } from '@shared/repo'; -import { courseFactory, setupEntities, userFactory } from '@shared/testing'; +import { courseFactory, groupFactory, roleFactory, setupEntities, userFactory } from '@shared/testing'; +import { RoomMemberService } from '@src/modules/room-member'; +import { roomFactory } from '@src/modules/room/testing'; +import { Permission, RoleName } from '@shared/domain/interface'; +import { GroupTypes } from '@src/modules/group'; +import { roomMemberFactory } from '@src/modules/room-member/testing'; import { columnFactory, columnBoardFactory } from '../../testing'; import { BoardExternalReferenceType, BoardRoles, UserWithBoardRoles } from '../../domain'; import { BoardContextService } from './board-context.service'; @@ -11,11 +16,16 @@ describe(`${BoardContextService.name}`, () => { let module: TestingModule; let service: BoardContextService; let courseRepo: DeepMocked; + let roomMemberService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ providers: [ BoardContextService, + { + provide: RoomMemberService, + useValue: createMock(), + }, { provide: CourseRepo, useValue: createMock(), @@ -24,6 +34,7 @@ describe(`${BoardContextService.name}`, () => { }).compile(); service = module.get(BoardContextService); + roomMemberService = module.get(RoomMemberService); courseRepo = module.get(CourseRepo); await setupEntities(); @@ -202,5 +213,111 @@ describe(`${BoardContextService.name}`, () => { }); }); }); + describe('when node has a room context', () => { + describe('when user with editor role is associated with the room', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const role = roleFactory.build({ name: RoleName.ROOM_EDITOR, permissions: [Permission.ROOM_EDIT] }); + const group = groupFactory.build({ type: GroupTypes.ROOM, users: [{ userId: user.id, roleId: role.id }] }); + const room = roomFactory.build(); + roomMemberFactory.build({ roomId: room.id, userGroupId: group.id }); + const columnBoard = columnBoardFactory.build({ + context: { id: room.id, type: BoardExternalReferenceType.Room }, + }); + + return { columnBoard, role, user }; + }; + + it('should return their information + editor role', async () => { + const { columnBoard, role, user } = setup(); + + roomMemberService.getRoomMemberAuthorizable.mockResolvedValue({ + id: 'foo', + roomId: columnBoard.context.id, + members: [{ userId: user.id, roles: [role] }], + }); + + const result = await service.getUsersWithBoardRoles(columnBoard); + const expected: UserWithBoardRoles[] = [ + { + userId: user.id, + roles: [BoardRoles.EDITOR], + }, + ]; + + expect(result).toEqual(expected); + }); + }); + + describe('when user with view role is associated with the room', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const role = roleFactory.build({ name: RoleName.ROOM_VIEWER, permissions: [Permission.ROOM_VIEW] }); + const group = groupFactory.build({ type: GroupTypes.ROOM, users: [{ userId: user.id, roleId: role.id }] }); + const room = roomFactory.build(); + roomMemberFactory.build({ roomId: room.id, userGroupId: group.id }); + const columnBoard = columnBoardFactory.build({ + context: { id: room.id, type: BoardExternalReferenceType.Room }, + }); + + return { columnBoard, role, user }; + }; + + it('should return their information + reader role', async () => { + const { columnBoard, role, user } = setup(); + + roomMemberService.getRoomMemberAuthorizable.mockResolvedValue({ + id: 'foo', + roomId: columnBoard.context.id, + members: [{ userId: user.id, roles: [role] }], + }); + + const result = await service.getUsersWithBoardRoles(columnBoard); + const expected: UserWithBoardRoles[] = [ + { + userId: user.id, + roles: [BoardRoles.READER], + }, + ]; + + expect(result).toEqual(expected); + }); + }); + + describe('when user with not-matching role is associated with the room', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const role = roleFactory.build(); + const group = groupFactory.build({ type: GroupTypes.ROOM, users: [{ userId: user.id, roleId: role.id }] }); + const room = roomFactory.build(); + roomMemberFactory.build({ roomId: room.id, userGroupId: group.id }); + const columnBoard = columnBoardFactory.build({ + context: { id: room.id, type: BoardExternalReferenceType.Room }, + }); + + return { columnBoard, role, user }; + }; + + it('should return their information + no role', async () => { + const { columnBoard, role, user } = setup(); + + roomMemberService.getRoomMemberAuthorizable.mockResolvedValue({ + id: 'foo', + roomId: columnBoard.context.id, + members: [{ userId: user.id, roles: [role] }], + }); + + const result = await service.getUsersWithBoardRoles(columnBoard); + const expected: UserWithBoardRoles[] = [ + { + userId: user.id, + roles: [], + }, + ]; + + expect(result).toEqual(expected); + }); + }); + }); }); }); From 5df2644b7f81c37822e262a7149a05761700e701 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Fri, 1 Nov 2024 20:28:33 +0100 Subject: [PATCH 12/20] add board room visibility response property --- .../modules/room/api/dto/response/room-board-item.response.ts | 4 ++++ apps/server/src/modules/room/api/mapper/room.mapper.ts | 1 + ...om-get-content.api.spec.ts => room-get-boards.api.spec.ts} | 4 +++- 3 files changed, 8 insertions(+), 1 deletion(-) rename apps/server/src/modules/room/api/test/{room-get-content.api.spec.ts => room-get-boards.api.spec.ts} (97%) diff --git a/apps/server/src/modules/room/api/dto/response/room-board-item.response.ts b/apps/server/src/modules/room/api/dto/response/room-board-item.response.ts index e6908aa884a..0840c1c1d87 100644 --- a/apps/server/src/modules/room/api/dto/response/room-board-item.response.ts +++ b/apps/server/src/modules/room/api/dto/response/room-board-item.response.ts @@ -11,6 +11,9 @@ export class RoomBoardItemResponse { @ApiProperty({ enum: BoardLayout, enumName: 'BoardLayout' }) layout: BoardLayout; + @ApiProperty({ type: Boolean }) + isVisible: boolean; + @ApiProperty({ type: Date }) createdAt: Date; @@ -21,6 +24,7 @@ export class RoomBoardItemResponse { this.id = item.id; this.title = item.title; this.layout = item.layout; + this.isVisible = item.isVisible; this.createdAt = item.createdAt; this.updatedAt = item.updatedAt; } diff --git a/apps/server/src/modules/room/api/mapper/room.mapper.ts b/apps/server/src/modules/room/api/mapper/room.mapper.ts index dad650f6c1e..02fd43a659f 100644 --- a/apps/server/src/modules/room/api/mapper/room.mapper.ts +++ b/apps/server/src/modules/room/api/mapper/room.mapper.ts @@ -51,6 +51,7 @@ export class RoomMapper { id: board.id, title: board.title, layout: board.layout, + isVisible: board.isVisible, createdAt: board.createdAt, updatedAt: board.updatedAt, }); diff --git a/apps/server/src/modules/room/api/test/room-get-content.api.spec.ts b/apps/server/src/modules/room/api/test/room-get-boards.api.spec.ts similarity index 97% rename from apps/server/src/modules/room/api/test/room-get-content.api.spec.ts rename to apps/server/src/modules/room/api/test/room-get-boards.api.spec.ts index 17251b879a7..b727b66355a 100644 --- a/apps/server/src/modules/room/api/test/room-get-content.api.spec.ts +++ b/apps/server/src/modules/room/api/test/room-get-boards.api.spec.ts @@ -119,7 +119,7 @@ describe('Room Controller (API)', () => { }; describe('when the room exists', () => { - it('should return a room', async () => { + it('should return the room boards', async () => { const { loggedInClient, room, board } = await setup(); const response = await loggedInClient.get(`${room.id}/boards`); @@ -127,6 +127,8 @@ describe('Room Controller (API)', () => { expect((response.body as { data: Record }).data[0]).toEqual({ id: board.id, title: board.title, + layout: board.layout, + isVisible: board.isVisible, createdAt: board.createdAt.toISOString(), updatedAt: board.updatedAt.toISOString(), }); From 54622616c2939a197ebf9d10e34825f21e226208 Mon Sep 17 00:00:00 2001 From: MartinSchuhmacher Date: Sun, 3 Nov 2024 13:14:15 +0100 Subject: [PATCH 13/20] Spliting board api tests between course and room context --- ...ts => board-context-in-course.api.spec.ts} | 2 +- .../board-context-in-rooms.api.spec.ts | 141 ++++++++++++ ...ec.ts => board-copy-in-course.api.spec.ts} | 6 +- .../api-test/board-copy-in-room.api.spec.ts | 179 +++++++++++++++ ....ts => board-delete-in-course.api.spec.ts} | 2 +- .../api-test/board-delete-in-room.api.spec.ts | 146 ++++++++++++ ....ts => board-lookup-in-course.api.spec.ts} | 2 +- .../api-test/board-lookup-in-room.api.spec.ts | 182 +++++++++++++++ ... board-update-title-in-course.api.spec.ts} | 6 +- .../board-update-title-in-room.api.spec.ts | 210 ++++++++++++++++++ ...=> board-visibility-in-course.api.spec.ts} | 6 +- .../board-visibility-in-room.api.spec.ts | 172 ++++++++++++++ .../internal/board-context.service.spec.ts | 1 + 13 files changed, 1043 insertions(+), 12 deletions(-) rename apps/server/src/modules/board/controller/api-test/{board-context.api.spec.ts => board-context-in-course.api.spec.ts} (97%) create mode 100644 apps/server/src/modules/board/controller/api-test/board-context-in-rooms.api.spec.ts rename apps/server/src/modules/board/controller/api-test/{board-copy.api.spec.ts => board-copy-in-course.api.spec.ts} (95%) create mode 100644 apps/server/src/modules/board/controller/api-test/board-copy-in-room.api.spec.ts rename apps/server/src/modules/board/controller/api-test/{board-delete.api.spec.ts => board-delete-in-course.api.spec.ts} (98%) create mode 100644 apps/server/src/modules/board/controller/api-test/board-delete-in-room.api.spec.ts rename apps/server/src/modules/board/controller/api-test/{board-lookup.api.spec.ts => board-lookup-in-course.api.spec.ts} (98%) create mode 100644 apps/server/src/modules/board/controller/api-test/board-lookup-in-room.api.spec.ts rename apps/server/src/modules/board/controller/api-test/{board-update-title.api.spec.ts => board-update-title-in-course.api.spec.ts} (96%) create mode 100644 apps/server/src/modules/board/controller/api-test/board-update-title-in-room.api.spec.ts rename apps/server/src/modules/board/controller/api-test/{board-visibility.api.spec.ts => board-visibility-in-course.api.spec.ts} (94%) create mode 100644 apps/server/src/modules/board/controller/api-test/board-visibility-in-room.api.spec.ts diff --git a/apps/server/src/modules/board/controller/api-test/board-context.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-context-in-course.api.spec.ts similarity index 97% rename from apps/server/src/modules/board/controller/api-test/board-context.api.spec.ts rename to apps/server/src/modules/board/controller/api-test/board-context-in-course.api.spec.ts index 9cf69fa7e76..1fc0caf98bc 100644 --- a/apps/server/src/modules/board/controller/api-test/board-context.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-context-in-course.api.spec.ts @@ -8,7 +8,7 @@ import { BoardExternalReferenceType } from '../../domain'; const baseRouteName = '/boards'; -describe('board get context (api)', () => { +describe('board get context in course (api)', () => { let app: INestApplication; let em: EntityManager; let testApiClient: TestApiClient; diff --git a/apps/server/src/modules/board/controller/api-test/board-context-in-rooms.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-context-in-rooms.api.spec.ts new file mode 100644 index 00000000000..1d701b7a157 --- /dev/null +++ b/apps/server/src/modules/board/controller/api-test/board-context-in-rooms.api.spec.ts @@ -0,0 +1,141 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { ServerTestModule } from '@modules/server'; +import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { TestApiClient, cleanupCollections, groupEntityFactory, roleFactory, userFactory } from '@shared/testing'; +import { Permission, RoleName } from '@shared/domain/interface'; +import { accountFactory } from '@src/modules/account/testing'; +import { GroupEntityTypes } from '@src/modules/group/entity'; +import { roomMemberEntityFactory } from '@src/modules/room-member/testing'; +import { roomEntityFactory } from '@src/modules/room/testing'; +import { columnBoardEntityFactory } from '../../testing'; +import { BoardExternalReferenceType } from '../../domain'; + +const baseRouteName = '/boards'; + +describe('board get context in room (api)', () => { + let app: INestApplication; + let em: EntityManager; + let testApiClient: TestApiClient; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ServerTestModule], + }).compile(); + + app = module.createNestApplication(); + await app.init(); + em = module.get(EntityManager); + testApiClient = new TestApiClient(app, baseRouteName); + }); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(async () => { + await cleanupCollections(em); + }); + + const setup = async () => { + const userWithEditRole = userFactory.buildWithId(); + const accountWithEditRole = accountFactory.withUser(userWithEditRole).build(); + + const userWithViewRole = userFactory.buildWithId(); + const accountWithViewRole = accountFactory.withUser(userWithViewRole).build(); + + const noAccessUser = userFactory.buildWithId(); + const noAccessAccount = accountFactory.withUser(noAccessUser).build(); + + const roleRoomEdit = roleFactory.buildWithId({ + name: RoleName.ROOM_EDITOR, + permissions: [Permission.ROOM_EDIT], + }); + const roleRoomView = roleFactory.buildWithId({ + name: RoleName.ROOM_VIEWER, + permissions: [Permission.ROOM_VIEW], + }); + + const userGroup = groupEntityFactory.buildWithId({ + type: GroupEntityTypes.ROOM, + users: [ + { user: userWithEditRole, role: roleRoomEdit }, + { user: userWithViewRole, role: roleRoomView }, + ], + }); + + const room = roomEntityFactory.buildWithId(); + + const roomMember = roomMemberEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + + await em.persistAndFlush([ + accountWithEditRole, + accountWithViewRole, + noAccessAccount, + userWithEditRole, + userWithViewRole, + noAccessUser, + roleRoomEdit, + roleRoomView, + userGroup, + room, + roomMember, + ]); + + const columnBoardNode = columnBoardEntityFactory.build({ + isVisible: false, + context: { id: room.id, type: BoardExternalReferenceType.Room }, + }); + + await em.persistAndFlush([columnBoardNode]); + em.clear(); + + return { accountWithEditRole, accountWithViewRole, noAccessAccount, columnBoardNode }; + }; + + describe('with user who has edit role in room', () => { + it('should return status 200', async () => { + const { accountWithEditRole, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithEditRole); + + const response = await loggedInClient.get(`${columnBoardNode.id}/context`); + + expect(response.status).toEqual(200); + }); + + it('should return the context', async () => { + const { accountWithEditRole, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithEditRole); + + const response = await loggedInClient.get(`${columnBoardNode.id}/context`); + + expect(response.body).toEqual({ id: columnBoardNode.context?.id, type: columnBoardNode.context?.type }); + }); + }); + + describe('with user who has only view role in room', () => { + it('should return status 403', async () => { + const { accountWithViewRole, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithViewRole); + + const response = await loggedInClient.get(`${columnBoardNode.id}/context`); + + expect(response.status).toEqual(403); + }); + }); + + describe('with user who is not part of the room', () => { + it('should return status 403', async () => { + const { noAccessAccount, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(noAccessAccount); + + const response = await loggedInClient.get(`${columnBoardNode.id}/context`); + + expect(response.status).toEqual(403); + }); + }); +}); diff --git a/apps/server/src/modules/board/controller/api-test/board-copy.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-copy-in-course.api.spec.ts similarity index 95% rename from apps/server/src/modules/board/controller/api-test/board-copy.api.spec.ts rename to apps/server/src/modules/board/controller/api-test/board-copy-in-course.api.spec.ts index 236b2d49bd1..51a5cf227c1 100644 --- a/apps/server/src/modules/board/controller/api-test/board-copy.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-copy-in-course.api.spec.ts @@ -10,7 +10,7 @@ import { BoardExternalReferenceType } from '../../domain'; const baseRouteName = '/boards'; -describe(`board copy (api)`, () => { +describe(`board copy with course relation (api)`, () => { let app: INestApplication; let em: EntityManager; let testApiClient: TestApiClient; @@ -39,13 +39,13 @@ describe(`board copy (api)`, () => { const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); const course = courseFactory.build({ teachers: [teacherUser] }); - await em.persistAndFlush([teacherUser, course]); + await em.persistAndFlush([teacherAccount, teacherUser, course]); const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - await em.persistAndFlush([teacherAccount, teacherUser, columnBoardNode]); + await em.persistAndFlush([columnBoardNode]); em.clear(); const loggedInClient = await testApiClient.login(teacherAccount); diff --git a/apps/server/src/modules/board/controller/api-test/board-copy-in-room.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-copy-in-room.api.spec.ts new file mode 100644 index 00000000000..b0f82521986 --- /dev/null +++ b/apps/server/src/modules/board/controller/api-test/board-copy-in-room.api.spec.ts @@ -0,0 +1,179 @@ +/* import { EntityManager } from '@mikro-orm/mongodb'; +import { ServerTestModule } from '@modules/server/server.module'; +import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { TestApiClient, cleanupCollections, groupEntityFactory, roleFactory, userFactory } from '@shared/testing'; +import { CopyApiResponse, CopyElementType, CopyStatusEnum } from '@modules/copy-helper'; +import { Permission, RoleName } from '@shared/domain/interface'; +import { accountFactory } from '@src/modules/account/testing'; +import { GroupEntityTypes } from '@src/modules/group/entity'; +import { roomMemberEntityFactory } from '@src/modules/room-member/testing'; +import { roomEntityFactory } from '@src/modules/room/testing'; +import { BoardNodeEntity } from '../../repo'; +import { BoardExternalReferenceType } from '../../domain'; +import { columnBoardEntityFactory } from '../../testing'; + +const baseRouteName = '/boards'; + +describe(`board copy with room relation (api)`, () => { + let app: INestApplication; + let em: EntityManager; + let testApiClient: TestApiClient; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ServerTestModule], + }).compile(); + + app = module.createNestApplication(); + await app.init(); + em = module.get(EntityManager); + testApiClient = new TestApiClient(app, baseRouteName); + }); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(async () => { + await cleanupCollections(em); + }); + + const setup = async () => { + const userWithEditRole = userFactory.buildWithId(); + const accountWithEditRole = accountFactory.withUser(userWithEditRole).build(); + + const userWithViewRole = userFactory.buildWithId(); + const accountWithViewRole = accountFactory.withUser(userWithViewRole).build(); + + const noAccessUser = userFactory.buildWithId(); + const noAccessAccount = accountFactory.withUser(noAccessUser).build(); + + const roleRoomEdit = roleFactory.buildWithId({ + name: RoleName.ROOM_EDITOR, + permissions: [Permission.ROOM_EDIT], + }); + const roleRoomView = roleFactory.buildWithId({ + name: RoleName.ROOM_VIEWER, + permissions: [Permission.ROOM_VIEW], + }); + + const userGroup = groupEntityFactory.buildWithId({ + type: GroupEntityTypes.ROOM, + users: [ + { user: userWithEditRole, role: roleRoomEdit }, + { user: userWithViewRole, role: roleRoomView }, + ], + }); + + const room = roomEntityFactory.buildWithId(); + + const roomMember = roomMemberEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + + await em.persistAndFlush([ + accountWithEditRole, + accountWithViewRole, + noAccessAccount, + userWithEditRole, + userWithViewRole, + noAccessUser, + roleRoomEdit, + roleRoomView, + userGroup, + room, + roomMember, + ]); + + const columnBoardNode = columnBoardEntityFactory.build({ + context: { id: room.id, type: BoardExternalReferenceType.Room }, + }); + + await em.persistAndFlush([columnBoardNode]); + em.clear(); + + return { accountWithEditRole, accountWithViewRole, noAccessAccount, columnBoardNode }; + }; + + describe('with user who has edit role in room', () => { + it('should return status 201', async () => { + const { accountWithEditRole, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithEditRole); + + const response = await loggedInClient.post(`${columnBoardNode.id}/copy`); + + expect(response.status).toEqual(201); + }); + + it('should actually copy the board', async () => { + const { accountWithEditRole, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithEditRole); + + const response = await loggedInClient.post(`${columnBoardNode.id}/copy`); + const body = response.body as CopyApiResponse; + + const expectedBody: CopyApiResponse = { + id: expect.any(String), + type: CopyElementType.COLUMNBOARD, + status: CopyStatusEnum.SUCCESS, + }; + + expect(body).toEqual(expectedBody); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const result = await em.findOneOrFail(BoardNodeEntity, body.id!); + + expect(result).toBeDefined(); + }); + + describe('with invalid id', () => { + it('should return status 400', async () => { + const { accountWithEditRole } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithEditRole); + + const response = await loggedInClient.post(`invalid-id/copy`); + + expect(response.status).toEqual(400); + }); + }); + + describe('with unknown id', () => { + it('should return status 404', async () => { + const { accountWithEditRole } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithEditRole); + + const response = await loggedInClient.post(`65e84684e43ba80204598425/copy`); + + expect(response.status).toEqual(404); + }); + }); + }); + + describe('with user who has only view role in room', () => { + it('should return status 403', async () => { + const { accountWithViewRole, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithViewRole); + + const response = await loggedInClient.post(`${columnBoardNode.id}/copy`); + + expect(response.status).toEqual(403); + }); + }); + + describe('with user who is not part of the room', () => { + it('should return status 403', async () => { + const { noAccessAccount, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(noAccessAccount); + + const response = await loggedInClient.post(`${columnBoardNode.id}/copy`); + + expect(response.status).toEqual(403); + }); + }); +}); + */ diff --git a/apps/server/src/modules/board/controller/api-test/board-delete.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-delete-in-course.api.spec.ts similarity index 98% rename from apps/server/src/modules/board/controller/api-test/board-delete.api.spec.ts rename to apps/server/src/modules/board/controller/api-test/board-delete-in-course.api.spec.ts index cb71b2a3863..7bcc32a50bf 100644 --- a/apps/server/src/modules/board/controller/api-test/board-delete.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-delete-in-course.api.spec.ts @@ -9,7 +9,7 @@ import { columnBoardEntityFactory, columnEntityFactory } from '../../testing'; const baseRouteName = '/boards'; -describe(`board delete (api)`, () => { +describe(`board delete in course (api)`, () => { let app: INestApplication; let em: EntityManager; let testApiClient: TestApiClient; diff --git a/apps/server/src/modules/board/controller/api-test/board-delete-in-room.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-delete-in-room.api.spec.ts new file mode 100644 index 00000000000..59d819352c8 --- /dev/null +++ b/apps/server/src/modules/board/controller/api-test/board-delete-in-room.api.spec.ts @@ -0,0 +1,146 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { ServerTestModule } from '@modules/server'; +import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { cleanupCollections, groupEntityFactory, roleFactory, TestApiClient, userFactory } from '@shared/testing'; +import { accountFactory } from '@src/modules/account/testing'; +import { Permission, RoleName } from '@shared/domain/interface'; +import { GroupEntityTypes } from '@src/modules/group/entity'; +import { roomEntityFactory } from '@src/modules/room/testing'; +import { roomMemberEntityFactory } from '@src/modules/room-member/testing'; +import { columnBoardEntityFactory, columnEntityFactory } from '../../testing'; +import { BoardNodeEntity } from '../../repo'; +import { BoardExternalReferenceType } from '../../domain'; + +const baseRouteName = '/boards'; + +describe(`board delete in room (api)`, () => { + let app: INestApplication; + let em: EntityManager; + let testApiClient: TestApiClient; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ServerTestModule], + }).compile(); + + app = module.createNestApplication(); + await app.init(); + em = module.get(EntityManager); + testApiClient = new TestApiClient(app, baseRouteName); + }); + + beforeEach(async () => { + await cleanupCollections(em); + }); + + afterAll(async () => { + await app.close(); + }); + + const setup = async () => { + const userWithEditRole = userFactory.buildWithId(); + const accountWithEditRole = accountFactory.withUser(userWithEditRole).build(); + + const userWithViewRole = userFactory.buildWithId(); + const accountWithViewRole = accountFactory.withUser(userWithViewRole).build(); + + const noAccessUser = userFactory.buildWithId(); + const noAccessAccount = accountFactory.withUser(noAccessUser).build(); + + const roleRoomEdit = roleFactory.buildWithId({ name: RoleName.ROOM_EDITOR, permissions: [Permission.ROOM_EDIT] }); + const roleRoomView = roleFactory.buildWithId({ name: RoleName.ROOM_VIEWER, permissions: [Permission.ROOM_VIEW] }); + + const userGroup = groupEntityFactory.buildWithId({ + type: GroupEntityTypes.ROOM, + users: [ + { user: userWithEditRole, role: roleRoomEdit }, + { user: userWithViewRole, role: roleRoomView }, + ], + }); + + const room = roomEntityFactory.buildWithId(); + + const roomMember = roomMemberEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + + await em.persistAndFlush([ + accountWithEditRole, + accountWithViewRole, + noAccessAccount, + userWithEditRole, + userWithViewRole, + noAccessUser, + roleRoomEdit, + roleRoomView, + userGroup, + room, + roomMember, + ]); + + const columnBoardNode = columnBoardEntityFactory.build({ + context: { id: room.id, type: BoardExternalReferenceType.Room }, + }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + + await em.persistAndFlush([columnBoardNode, columnNode]); + em.clear(); + + return { accountWithEditRole, accountWithViewRole, noAccessAccount, columnBoardNode, columnNode }; + }; + + describe('with valid user which is allowed to edit room', () => { + it('should return status 204', async () => { + const { accountWithEditRole, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithEditRole); + + const response = await loggedInClient.delete(columnBoardNode.id); + + expect(response.status).toEqual(204); + }); + + it('should actually delete the board', async () => { + const { accountWithEditRole, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithEditRole); + + await loggedInClient.delete(columnBoardNode.id); + + await expect(em.findOneOrFail(BoardNodeEntity, columnBoardNode.id)).rejects.toThrow(); + }); + + it('should actually delete columns of the board', async () => { + const { accountWithEditRole, columnNode, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithEditRole); + + await loggedInClient.delete(columnBoardNode.id); + + await expect(em.findOneOrFail(BoardNodeEntity, columnNode.id)).rejects.toThrow(); + }); + }); + + describe('with invalid user who has only view rights to the room', () => { + it('should return status 403', async () => { + const { accountWithViewRole, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithViewRole); + + const response = await loggedInClient.delete(columnBoardNode.id); + + expect(response.status).toEqual(403); + }); + }); + + describe('with invalid user who has no access to the room', () => { + it('should return status 403', async () => { + const { noAccessAccount, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(noAccessAccount); + + const response = await loggedInClient.delete(columnBoardNode.id); + + expect(response.status).toEqual(403); + }); + }); +}); diff --git a/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-lookup-in-course.api.spec.ts similarity index 98% rename from apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts rename to apps/server/src/modules/board/controller/api-test/board-lookup-in-course.api.spec.ts index 25ae2ea4866..b6e0419ace9 100644 --- a/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-lookup-in-course.api.spec.ts @@ -10,7 +10,7 @@ import { BoardResponse } from '../dto'; const baseRouteName = '/boards'; -describe(`board lookup (api)`, () => { +describe(`board lookup in course (api)`, () => { let app: INestApplication; let em: EntityManager; let testApiClient: TestApiClient; diff --git a/apps/server/src/modules/board/controller/api-test/board-lookup-in-room.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-lookup-in-room.api.spec.ts new file mode 100644 index 00000000000..46f2acb7ae5 --- /dev/null +++ b/apps/server/src/modules/board/controller/api-test/board-lookup-in-room.api.spec.ts @@ -0,0 +1,182 @@ +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { ServerTestModule } from '@modules/server/server.module'; +import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { cleanupCollections, groupEntityFactory, roleFactory, TestApiClient, userFactory } from '@shared/testing'; + +import { Permission, RoleName } from '@shared/domain/interface'; +import { accountFactory } from '@src/modules/account/testing'; +import { GroupEntityTypes } from '@src/modules/group/entity'; +import { roomMemberEntityFactory } from '@src/modules/room-member/testing'; +import { roomEntityFactory } from '@src/modules/room/testing'; +import { BoardExternalReferenceType, BoardLayout } from '../../domain'; +import { BoardResponse } from '../dto'; +import { cardEntityFactory, columnEntityFactory, columnBoardEntityFactory } from '../../testing'; + +const baseRouteName = '/boards'; + +describe(`board lookup in room (api)`, () => { + let app: INestApplication; + let em: EntityManager; + let testApiClient: TestApiClient; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ServerTestModule], + }).compile(); + + app = module.createNestApplication(); + await app.init(); + em = module.get(EntityManager); + testApiClient = new TestApiClient(app, baseRouteName); + }); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(async () => { + await cleanupCollections(em); + }); + + const setup = async () => { + const userWithEditRole = userFactory.buildWithId(); + const accountWithEditRole = accountFactory.withUser(userWithEditRole).build(); + + const userWithViewRole = userFactory.buildWithId(); + const accountWithViewRole = accountFactory.withUser(userWithViewRole).build(); + + const noAccessUser = userFactory.buildWithId(); + const noAccessAccount = accountFactory.withUser(noAccessUser).build(); + + const roleRoomEdit = roleFactory.buildWithId({ name: RoleName.ROOM_EDITOR, permissions: [Permission.ROOM_EDIT] }); + const roleRoomView = roleFactory.buildWithId({ name: RoleName.ROOM_VIEWER, permissions: [Permission.ROOM_VIEW] }); + + const userGroup = groupEntityFactory.buildWithId({ + type: GroupEntityTypes.ROOM, + users: [ + { user: userWithEditRole, role: roleRoomEdit }, + { user: userWithViewRole, role: roleRoomView }, + ], + }); + + const room = roomEntityFactory.buildWithId(); + + const roomMember = roomMemberEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + + await em.persistAndFlush([ + accountWithEditRole, + accountWithViewRole, + noAccessAccount, + userWithEditRole, + userWithViewRole, + noAccessUser, + roleRoomEdit, + roleRoomView, + userGroup, + room, + roomMember, + ]); + + const columnBoardNode = columnBoardEntityFactory.build({ + context: { id: room.id, type: BoardExternalReferenceType.Room }, + }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + const cardNode1 = cardEntityFactory.withParent(columnNode).build(); + const cardNode2 = cardEntityFactory.withParent(columnNode).build(); + const cardNode3 = cardEntityFactory.withParent(columnNode).build(); + const notOfThisBoardCardNode = cardEntityFactory.build(); + + await em.persistAndFlush([columnBoardNode, columnNode, cardNode1, cardNode2, cardNode3, notOfThisBoardCardNode]); + em.clear(); + + return { + accountWithEditRole, + accountWithViewRole, + noAccessAccount, + columnBoardNode, + columnNode, + card1: cardNode1, + card2: cardNode2, + card3: cardNode3, + }; + }; + + describe('When user has edit rights in room', () => { + describe('with valid board id', () => { + it('should return status 200', async () => { + const { accountWithEditRole, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithEditRole); + + const response = await loggedInClient.get(columnBoardNode.id); + + expect(response.status).toEqual(200); + }); + + it('should return the correct board', async () => { + const { accountWithEditRole, columnBoardNode, columnNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithEditRole); + + const response = await loggedInClient.get(columnBoardNode.id); + const result = response.body as BoardResponse; + + expect(result.id).toEqual(columnBoardNode.id); + expect(result.columns).toHaveLength(1); + expect(result.columns[0].id).toEqual(columnNode.id); + expect(result.columns[0].cards).toHaveLength(3); + }); + }); + + describe('board layout', () => { + it(`should default to ${BoardLayout.COLUMNS}`, async () => { + const { accountWithEditRole, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithEditRole); + + const response = await loggedInClient.get(columnBoardNode.id); + const result = response.body as BoardResponse; + + expect(result.layout).toEqual(BoardLayout.COLUMNS); + }); + }); + + describe('with invalid board id', () => { + it('should return status 404', async () => { + const { accountWithEditRole } = await setup(); + const notExistingBoardId = new ObjectId().toString(); + + const loggedInClient = await testApiClient.login(accountWithEditRole); + + const response = await loggedInClient.get(notExistingBoardId); + + expect(response.status).toEqual(404); + }); + }); + }); + + describe('When user has only view rights in room', () => { + it('should return status 403', async () => { + const { accountWithViewRole, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithViewRole); + + const response = await loggedInClient.get(columnBoardNode.id); + + expect(response.status).toEqual(403); + }); + }); + + describe('When user does not belong to room', () => { + it('should return status 403', async () => { + const { noAccessAccount, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(noAccessAccount); + + const response = await loggedInClient.get(columnBoardNode.id); + + expect(response.status).toEqual(403); + }); + }); +}); diff --git a/apps/server/src/modules/board/controller/api-test/board-update-title.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-update-title-in-course.api.spec.ts similarity index 96% rename from apps/server/src/modules/board/controller/api-test/board-update-title.api.spec.ts rename to apps/server/src/modules/board/controller/api-test/board-update-title-in-course.api.spec.ts index 93254af4884..57b5489e707 100644 --- a/apps/server/src/modules/board/controller/api-test/board-update-title.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-update-title-in-course.api.spec.ts @@ -10,7 +10,7 @@ import { BoardExternalReferenceType } from '../../domain'; const baseRouteName = '/boards'; -describe(`board update title (api)`, () => { +describe(`board update title with course relation (api)`, () => { let app: INestApplication; let em: EntityManager; let testApiClient: TestApiClient; @@ -39,13 +39,13 @@ describe(`board update title (api)`, () => { const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); const course = courseFactory.build({ teachers: [teacherUser] }); - await em.persistAndFlush([teacherUser, course]); + await em.persistAndFlush([teacherAccount, teacherUser, course]); const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - await em.persistAndFlush([teacherAccount, teacherUser, columnBoardNode]); + await em.persistAndFlush([columnBoardNode]); em.clear(); const loggedInClient = await testApiClient.login(teacherAccount); diff --git a/apps/server/src/modules/board/controller/api-test/board-update-title-in-room.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-update-title-in-room.api.spec.ts new file mode 100644 index 00000000000..d5988437838 --- /dev/null +++ b/apps/server/src/modules/board/controller/api-test/board-update-title-in-room.api.spec.ts @@ -0,0 +1,210 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { ServerTestModule } from '@modules/server/server.module'; +import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ApiValidationError } from '@shared/common'; +import { TestApiClient, cleanupCollections, groupEntityFactory, roleFactory, userFactory } from '@shared/testing'; +import { Permission, RoleName } from '@shared/domain/interface'; +import { accountFactory } from '@src/modules/account/testing'; +import { GroupEntityTypes } from '@src/modules/group/entity'; +import { roomMemberEntityFactory } from '@src/modules/room-member/testing'; +import { roomEntityFactory } from '@src/modules/room/testing'; +import { BoardNodeEntity } from '../../repo'; +import { columnBoardEntityFactory } from '../../testing'; +import { BoardExternalReferenceType } from '../../domain'; + +const baseRouteName = '/boards'; + +describe(`board update title with room relation (api)`, () => { + let app: INestApplication; + let em: EntityManager; + let testApiClient: TestApiClient; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ServerTestModule], + }).compile(); + + app = module.createNestApplication(); + await app.init(); + em = module.get(EntityManager); + testApiClient = new TestApiClient(app, baseRouteName); + }); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(async () => { + await cleanupCollections(em); + }); + + const setup = async () => { + const userWithEditRole = userFactory.buildWithId(); + const accountWithEditRole = accountFactory.withUser(userWithEditRole).build(); + + const userWithViewRole = userFactory.buildWithId(); + const accountWithViewRole = accountFactory.withUser(userWithViewRole).build(); + + const noAccessUser = userFactory.buildWithId(); + const noAccessAccount = accountFactory.withUser(noAccessUser).build(); + + const roleRoomEdit = roleFactory.buildWithId({ + name: RoleName.ROOM_EDITOR, + permissions: [Permission.ROOM_EDIT], + }); + const roleRoomView = roleFactory.buildWithId({ + name: RoleName.ROOM_VIEWER, + permissions: [Permission.ROOM_VIEW], + }); + + const userGroup = groupEntityFactory.buildWithId({ + type: GroupEntityTypes.ROOM, + users: [ + { user: userWithEditRole, role: roleRoomEdit }, + { user: userWithViewRole, role: roleRoomView }, + ], + }); + + const room = roomEntityFactory.buildWithId(); + + const roomMember = roomMemberEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + + await em.persistAndFlush([ + accountWithEditRole, + accountWithViewRole, + noAccessAccount, + userWithEditRole, + userWithViewRole, + noAccessUser, + roleRoomEdit, + roleRoomView, + userGroup, + room, + roomMember, + ]); + + const originalTitle = 'old title'; + const columnBoardNode = columnBoardEntityFactory.build({ + title: originalTitle, + context: { id: room.id, type: BoardExternalReferenceType.Room }, + }); + + await em.persistAndFlush([columnBoardNode]); + em.clear(); + + return { accountWithEditRole, accountWithViewRole, noAccessAccount, columnBoardNode, originalTitle }; + }; + + describe('with user who has edit role in room', () => { + it('should return status 204', async () => { + const { accountWithEditRole, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithEditRole); + + const newTitle = 'new title'; + + const response = await loggedInClient.patch(`${columnBoardNode.id}/title`, { title: newTitle }); + + expect(response.status).toEqual(204); + }); + + it('should actually change the board title', async () => { + const { accountWithEditRole, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithEditRole); + + const newTitle = 'new title'; + + await loggedInClient.patch(`${columnBoardNode.id}/title`, { title: newTitle }); + + const result = await em.findOneOrFail(BoardNodeEntity, columnBoardNode.id); + + expect(result.title).toEqual(newTitle); + }); + + it('should sanitize the title', async () => { + const { accountWithEditRole, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithEditRole); + + const unsanitizedTitle = ' bar'; + const sanitizedTitle = 'foo bar'; + + await loggedInClient.patch(`${columnBoardNode.id}/title`, { title: unsanitizedTitle }); + const result = await em.findOneOrFail(BoardNodeEntity, columnBoardNode.id); + + expect(result.title).toEqual(sanitizedTitle); + }); + + it('should return status 400 when title is too long', async () => { + const { accountWithEditRole, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithEditRole); + + const newTitle = 'a'.repeat(101); + + const response = await loggedInClient.patch(`${columnBoardNode.id}/title`, { title: newTitle }); + + expect((response.body as ApiValidationError).validationErrors).toEqual([ + { + errors: ['title must be shorter than or equal to 100 characters'], + field: ['title'], + }, + ]); + expect(response.status).toEqual(400); + }); + + it('should return status 400 when title is empty string', async () => { + const { accountWithEditRole, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithEditRole); + + const newTitle = ''; + + const response = await loggedInClient.patch(`${columnBoardNode.id}/title`, { title: newTitle }); + + expect((response.body as ApiValidationError).validationErrors).toEqual([ + { + errors: ['title must be longer than or equal to 1 characters'], + field: ['title'], + }, + ]); + expect(response.status).toEqual(400); + }); + }); + + describe('with user who has only view role in room', () => { + it('should return status 403', async () => { + const { accountWithViewRole, columnBoardNode, originalTitle } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithViewRole); + + const newTitle = 'new title'; + + const response = await loggedInClient.patch(`${columnBoardNode.id}/title`, { title: newTitle }); + + expect(response.status).toEqual(403); + + const result = await em.findOneOrFail(BoardNodeEntity, columnBoardNode.id); + expect(result.title).toEqual(originalTitle); + }); + }); + + describe('with user who is not part of the room', () => { + it('should return status 403', async () => { + const { noAccessAccount, columnBoardNode, originalTitle } = await setup(); + + const loggedInClient = await testApiClient.login(noAccessAccount); + + const newTitle = 'new title'; + + const response = await loggedInClient.patch(`${columnBoardNode.id}/title`, { title: newTitle }); + + expect(response.status).toEqual(403); + + const result = await em.findOneOrFail(BoardNodeEntity, columnBoardNode.id); + expect(result.title).toEqual(originalTitle); + }); + }); +}); diff --git a/apps/server/src/modules/board/controller/api-test/board-visibility.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-visibility-in-course.api.spec.ts similarity index 94% rename from apps/server/src/modules/board/controller/api-test/board-visibility.api.spec.ts rename to apps/server/src/modules/board/controller/api-test/board-visibility-in-course.api.spec.ts index 5af52adbc7b..36126152bb7 100644 --- a/apps/server/src/modules/board/controller/api-test/board-visibility.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-visibility-in-course.api.spec.ts @@ -9,7 +9,7 @@ import { BoardExternalReferenceType } from '../../domain'; const baseRouteName = '/boards'; -describe(`board update visibility (api)`, () => { +describe(`board update visibility with course relation (api)`, () => { let app: INestApplication; let em: EntityManager; let testApiClient: TestApiClient; @@ -38,14 +38,14 @@ describe(`board update visibility (api)`, () => { const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); const course = courseFactory.build({ teachers: [teacherUser] }); - await em.persistAndFlush([teacherUser, course]); + await em.persistAndFlush([teacherAccount, teacherUser, course]); const columnBoardNode = columnBoardEntityFactory.build({ isVisible: false, context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - await em.persistAndFlush([teacherAccount, teacherUser, columnBoardNode]); + await em.persistAndFlush([columnBoardNode]); em.clear(); const loggedInClient = await testApiClient.login(teacherAccount); diff --git a/apps/server/src/modules/board/controller/api-test/board-visibility-in-room.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-visibility-in-room.api.spec.ts new file mode 100644 index 00000000000..c9a6aec631a --- /dev/null +++ b/apps/server/src/modules/board/controller/api-test/board-visibility-in-room.api.spec.ts @@ -0,0 +1,172 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { ServerTestModule } from '@modules/server/server.module'; +import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { TestApiClient, cleanupCollections, groupEntityFactory, roleFactory, userFactory } from '@shared/testing'; +import { Permission, RoleName } from '@shared/domain/interface'; +import { accountFactory } from '@src/modules/account/testing'; +import { GroupEntityTypes } from '@src/modules/group/entity'; +import { roomMemberEntityFactory } from '@src/modules/room-member/testing'; +import { roomEntityFactory } from '@src/modules/room/testing'; +import { BoardExternalReferenceType } from '../../domain'; +import { columnBoardEntityFactory } from '../../testing'; +import { BoardNodeEntity } from '../../repo'; + +const baseRouteName = '/boards'; + +describe(`board update visibility with room relation (api)`, () => { + let app: INestApplication; + let em: EntityManager; + let testApiClient: TestApiClient; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ServerTestModule], + }).compile(); + + app = module.createNestApplication(); + await app.init(); + em = module.get(EntityManager); + testApiClient = new TestApiClient(app, baseRouteName); + }); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(async () => { + await cleanupCollections(em); + }); + + const setup = async () => { + const userWithEditRole = userFactory.buildWithId(); + const accountWithEditRole = accountFactory.withUser(userWithEditRole).build(); + + const userWithViewRole = userFactory.buildWithId(); + const accountWithViewRole = accountFactory.withUser(userWithViewRole).build(); + + const noAccessUser = userFactory.buildWithId(); + const noAccessAccount = accountFactory.withUser(noAccessUser).build(); + + const roleRoomEdit = roleFactory.buildWithId({ + name: RoleName.ROOM_EDITOR, + permissions: [Permission.ROOM_EDIT], + }); + const roleRoomView = roleFactory.buildWithId({ + name: RoleName.ROOM_VIEWER, + permissions: [Permission.ROOM_VIEW], + }); + + const userGroup = groupEntityFactory.buildWithId({ + type: GroupEntityTypes.ROOM, + users: [ + { user: userWithEditRole, role: roleRoomEdit }, + { user: userWithViewRole, role: roleRoomView }, + ], + }); + + const room = roomEntityFactory.buildWithId(); + + const roomMember = roomMemberEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + + await em.persistAndFlush([ + accountWithEditRole, + accountWithViewRole, + noAccessAccount, + userWithEditRole, + userWithViewRole, + noAccessUser, + roleRoomEdit, + roleRoomView, + userGroup, + room, + roomMember, + ]); + + const columnBoardNode = columnBoardEntityFactory.build({ + isVisible: false, + context: { id: room.id, type: BoardExternalReferenceType.Room }, + }); + + await em.persistAndFlush([columnBoardNode]); + em.clear(); + + return { accountWithEditRole, accountWithViewRole, noAccessAccount, columnBoardNode }; + }; + + describe('with user who has edit role in room', () => { + it('should return status 204', async () => { + const { accountWithEditRole, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithEditRole); + + const isVisible = true; + + const response = await loggedInClient.patch(`${columnBoardNode.id}/visibility`, { isVisible }); + + expect(response.status).toEqual(204); + }); + + it('should actually change the board visibility', async () => { + const { accountWithEditRole, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithEditRole); + + const isVisible = true; + + await loggedInClient.patch(`${columnBoardNode.id}/visibility`, { isVisible }); + + const result = await em.findOneOrFail(BoardNodeEntity, columnBoardNode.id); + + expect(result.isVisible).toEqual(isVisible); + }); + }); + + describe('with user who has only view role in room', () => { + it('should return status 403', async () => { + const { accountWithViewRole, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithViewRole); + + const isVisible = true; + + const response = await loggedInClient.patch(`${columnBoardNode.id}/visibility`, { isVisible }); + + expect(response.status).toEqual(403); + }); + it('should not change the board visibility', async () => { + const { accountWithViewRole, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(accountWithViewRole); + + const isVisible = true; + await loggedInClient.patch(`${columnBoardNode.id}/visibility`, { isVisible }); + const result = await em.findOneOrFail(BoardNodeEntity, columnBoardNode.id); + expect(result.isVisible).toEqual(false); + }); + }); + + describe('with user who is not part of the room', () => { + it('should return status 403', async () => { + const { noAccessAccount, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(noAccessAccount); + + const isVisible = true; + + const response = await loggedInClient.patch(`${columnBoardNode.id}/visibility`, { isVisible }); + + expect(response.status).toEqual(403); + }); + it('should not change the board visibility', async () => { + const { noAccessAccount, columnBoardNode } = await setup(); + + const loggedInClient = await testApiClient.login(noAccessAccount); + + const isVisible = true; + await loggedInClient.patch(`${columnBoardNode.id}/visibility`, { isVisible }); + const result = await em.findOneOrFail(BoardNodeEntity, columnBoardNode.id); + expect(result.isVisible).toEqual(false); + }); + }); +}); diff --git a/apps/server/src/modules/board/service/internal/board-context.service.spec.ts b/apps/server/src/modules/board/service/internal/board-context.service.spec.ts index a3e72571378..c14ec70c1e1 100644 --- a/apps/server/src/modules/board/service/internal/board-context.service.spec.ts +++ b/apps/server/src/modules/board/service/internal/board-context.service.spec.ts @@ -213,6 +213,7 @@ describe(`${BoardContextService.name}`, () => { }); }); }); + describe('when node has a room context', () => { describe('when user with editor role is associated with the room', () => { const setup = () => { From 2fb9f2bdf7cfe9ed1273fb035c3cd8a25051bad8 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Mon, 4 Nov 2024 10:57:46 +0100 Subject: [PATCH 14/20] add roommember module to board ws api --- apps/server/src/modules/board/board-ws-api.module.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/server/src/modules/board/board-ws-api.module.ts b/apps/server/src/modules/board/board-ws-api.module.ts index 74b5a2b45b2..4a0bfe63cd4 100644 --- a/apps/server/src/modules/board/board-ws-api.module.ts +++ b/apps/server/src/modules/board/board-ws-api.module.ts @@ -1,16 +1,17 @@ +import { AuthorizationModule } from '@modules/authorization'; +import { UserModule } from '@modules/user'; import { forwardRef, Module } from '@nestjs/common'; import { CourseRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { AuthorizationModule } from '@modules/authorization'; -import { UserModule } from '@modules/user'; +import { RoomMemberModule } from '../room-member'; import { BoardModule } from './board.module'; import { BoardCollaborationGateway } from './gateway/board-collaboration.gateway'; import { MetricsService } from './metrics/metrics.service'; -import { BoardUc, CardUc, ColumnUc, ElementUc } from './uc'; import { BoardNodePermissionService } from './service'; +import { BoardUc, CardUc, ColumnUc, ElementUc } from './uc'; @Module({ - imports: [BoardModule, forwardRef(() => AuthorizationModule), LoggerModule, UserModule], + imports: [BoardModule, forwardRef(() => AuthorizationModule), LoggerModule, UserModule, RoomMemberModule], providers: [ BoardCollaborationGateway, BoardNodePermissionService, From 51d6d4eef7749c6c6e63bdab416664a19e00658f Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Mon, 4 Nov 2024 16:39:35 +0100 Subject: [PATCH 15/20] add missing import --- apps/server/src/modules/room/api/room.uc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/modules/room/api/room.uc.ts b/apps/server/src/modules/room/api/room.uc.ts index 7eb474a12e4..1691df897a9 100644 --- a/apps/server/src/modules/room/api/room.uc.ts +++ b/apps/server/src/modules/room/api/room.uc.ts @@ -7,7 +7,7 @@ import { FeatureDisabledLoggableException } from '@shared/common/loggable-except import { Page, UserDO } from '@shared/domain/domainobject'; import { IFindOptions, Permission, RoleName, RoomRole } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { BoardExternalReferenceType, ColumnBoardService } from '@src/modules/board'; +import { BoardExternalReferenceType, ColumnBoard, ColumnBoardService } from '@src/modules/board'; import { Room, RoomCreateProps, RoomService, RoomUpdateProps } from '../domain'; import { RoomConfig } from '../room.config'; import { RoomMemberResponse } from './dto/response/room-member.response'; From 1a507c267dca6a27d547f5d1b55ff3b41689ec5e Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Wed, 6 Nov 2024 11:38:04 +0100 Subject: [PATCH 16/20] fix tests --- .../api-test/board-copy-in-room.api.spec.ts | 179 ------------------ .../api-test/board-lookup-in-room.api.spec.ts | 4 +- .../src/modules/board/uc/board.uc.spec.ts | 5 + .../src/modules/room/api/room.uc.spec.ts | 9 +- 4 files changed, 14 insertions(+), 183 deletions(-) delete mode 100644 apps/server/src/modules/board/controller/api-test/board-copy-in-room.api.spec.ts diff --git a/apps/server/src/modules/board/controller/api-test/board-copy-in-room.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-copy-in-room.api.spec.ts deleted file mode 100644 index b0f82521986..00000000000 --- a/apps/server/src/modules/board/controller/api-test/board-copy-in-room.api.spec.ts +++ /dev/null @@ -1,179 +0,0 @@ -/* import { EntityManager } from '@mikro-orm/mongodb'; -import { ServerTestModule } from '@modules/server/server.module'; -import { INestApplication } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { TestApiClient, cleanupCollections, groupEntityFactory, roleFactory, userFactory } from '@shared/testing'; -import { CopyApiResponse, CopyElementType, CopyStatusEnum } from '@modules/copy-helper'; -import { Permission, RoleName } from '@shared/domain/interface'; -import { accountFactory } from '@src/modules/account/testing'; -import { GroupEntityTypes } from '@src/modules/group/entity'; -import { roomMemberEntityFactory } from '@src/modules/room-member/testing'; -import { roomEntityFactory } from '@src/modules/room/testing'; -import { BoardNodeEntity } from '../../repo'; -import { BoardExternalReferenceType } from '../../domain'; -import { columnBoardEntityFactory } from '../../testing'; - -const baseRouteName = '/boards'; - -describe(`board copy with room relation (api)`, () => { - let app: INestApplication; - let em: EntityManager; - let testApiClient: TestApiClient; - - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [ServerTestModule], - }).compile(); - - app = module.createNestApplication(); - await app.init(); - em = module.get(EntityManager); - testApiClient = new TestApiClient(app, baseRouteName); - }); - - afterAll(async () => { - await app.close(); - }); - - beforeEach(async () => { - await cleanupCollections(em); - }); - - const setup = async () => { - const userWithEditRole = userFactory.buildWithId(); - const accountWithEditRole = accountFactory.withUser(userWithEditRole).build(); - - const userWithViewRole = userFactory.buildWithId(); - const accountWithViewRole = accountFactory.withUser(userWithViewRole).build(); - - const noAccessUser = userFactory.buildWithId(); - const noAccessAccount = accountFactory.withUser(noAccessUser).build(); - - const roleRoomEdit = roleFactory.buildWithId({ - name: RoleName.ROOM_EDITOR, - permissions: [Permission.ROOM_EDIT], - }); - const roleRoomView = roleFactory.buildWithId({ - name: RoleName.ROOM_VIEWER, - permissions: [Permission.ROOM_VIEW], - }); - - const userGroup = groupEntityFactory.buildWithId({ - type: GroupEntityTypes.ROOM, - users: [ - { user: userWithEditRole, role: roleRoomEdit }, - { user: userWithViewRole, role: roleRoomView }, - ], - }); - - const room = roomEntityFactory.buildWithId(); - - const roomMember = roomMemberEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); - - await em.persistAndFlush([ - accountWithEditRole, - accountWithViewRole, - noAccessAccount, - userWithEditRole, - userWithViewRole, - noAccessUser, - roleRoomEdit, - roleRoomView, - userGroup, - room, - roomMember, - ]); - - const columnBoardNode = columnBoardEntityFactory.build({ - context: { id: room.id, type: BoardExternalReferenceType.Room }, - }); - - await em.persistAndFlush([columnBoardNode]); - em.clear(); - - return { accountWithEditRole, accountWithViewRole, noAccessAccount, columnBoardNode }; - }; - - describe('with user who has edit role in room', () => { - it('should return status 201', async () => { - const { accountWithEditRole, columnBoardNode } = await setup(); - - const loggedInClient = await testApiClient.login(accountWithEditRole); - - const response = await loggedInClient.post(`${columnBoardNode.id}/copy`); - - expect(response.status).toEqual(201); - }); - - it('should actually copy the board', async () => { - const { accountWithEditRole, columnBoardNode } = await setup(); - - const loggedInClient = await testApiClient.login(accountWithEditRole); - - const response = await loggedInClient.post(`${columnBoardNode.id}/copy`); - const body = response.body as CopyApiResponse; - - const expectedBody: CopyApiResponse = { - id: expect.any(String), - type: CopyElementType.COLUMNBOARD, - status: CopyStatusEnum.SUCCESS, - }; - - expect(body).toEqual(expectedBody); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const result = await em.findOneOrFail(BoardNodeEntity, body.id!); - - expect(result).toBeDefined(); - }); - - describe('with invalid id', () => { - it('should return status 400', async () => { - const { accountWithEditRole } = await setup(); - - const loggedInClient = await testApiClient.login(accountWithEditRole); - - const response = await loggedInClient.post(`invalid-id/copy`); - - expect(response.status).toEqual(400); - }); - }); - - describe('with unknown id', () => { - it('should return status 404', async () => { - const { accountWithEditRole } = await setup(); - - const loggedInClient = await testApiClient.login(accountWithEditRole); - - const response = await loggedInClient.post(`65e84684e43ba80204598425/copy`); - - expect(response.status).toEqual(404); - }); - }); - }); - - describe('with user who has only view role in room', () => { - it('should return status 403', async () => { - const { accountWithViewRole, columnBoardNode } = await setup(); - - const loggedInClient = await testApiClient.login(accountWithViewRole); - - const response = await loggedInClient.post(`${columnBoardNode.id}/copy`); - - expect(response.status).toEqual(403); - }); - }); - - describe('with user who is not part of the room', () => { - it('should return status 403', async () => { - const { noAccessAccount, columnBoardNode } = await setup(); - - const loggedInClient = await testApiClient.login(noAccessAccount); - - const response = await loggedInClient.post(`${columnBoardNode.id}/copy`); - - expect(response.status).toEqual(403); - }); - }); -}); - */ diff --git a/apps/server/src/modules/board/controller/api-test/board-lookup-in-room.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-lookup-in-room.api.spec.ts index 46f2acb7ae5..c6701f81c94 100644 --- a/apps/server/src/modules/board/controller/api-test/board-lookup-in-room.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-lookup-in-room.api.spec.ts @@ -157,14 +157,14 @@ describe(`board lookup in room (api)`, () => { }); describe('When user has only view rights in room', () => { - it('should return status 403', async () => { + it('should return status 200', async () => { const { accountWithViewRole, columnBoardNode } = await setup(); const loggedInClient = await testApiClient.login(accountWithViewRole); const response = await loggedInClient.get(columnBoardNode.id); - expect(response.status).toEqual(403); + expect(response.status).toEqual(200); }); }); diff --git a/apps/server/src/modules/board/uc/board.uc.spec.ts b/apps/server/src/modules/board/uc/board.uc.spec.ts index 26e8b220931..7042425d7aa 100644 --- a/apps/server/src/modules/board/uc/board.uc.spec.ts +++ b/apps/server/src/modules/board/uc/board.uc.spec.ts @@ -12,6 +12,7 @@ import { BoardExternalReferenceType, BoardLayout, BoardNodeFactory, Column, Colu import { BoardNodePermissionService, BoardNodeService, ColumnBoardService } from '../service'; import { columnBoardFactory, columnFactory } from '../testing'; import { BoardUc } from './board.uc'; +import { RoomMemberService } from '@src/modules/room-member'; describe(BoardUc.name, () => { let module: TestingModule; @@ -51,6 +52,10 @@ describe(BoardUc.name, () => { provide: BoardNodeFactory, useValue: createMock(), }, + { + provide: RoomMemberService, + useValue: createMock(), + }, { provide: LegacyLogger, useValue: createMock(), diff --git a/apps/server/src/modules/room/api/room.uc.spec.ts b/apps/server/src/modules/room/api/room.uc.spec.ts index cafa81465eb..ab0e958ae6c 100644 --- a/apps/server/src/modules/room/api/room.uc.spec.ts +++ b/apps/server/src/modules/room/api/room.uc.spec.ts @@ -1,4 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AuthorizationService } from '@modules/authorization'; +import { RoomMemberRepo, RoomMemberService } from '@modules/room-member'; import { UserService } from '@modules/user'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; @@ -6,8 +8,7 @@ import { FeatureDisabledLoggableException } from '@shared/common/loggable-except import { Page } from '@shared/domain/domainobject'; import { IFindOptions } from '@shared/domain/interface'; import { setupEntities, userFactory } from '@shared/testing'; -import { AuthorizationService } from '@modules/authorization'; -import { RoomMemberRepo, RoomMemberService } from '@modules/room-member'; +import { ColumnBoardService } from '@src/modules/board'; import { Room, RoomService } from '../domain'; import { RoomColor } from '../domain/type'; import { roomFactory } from '../testing'; @@ -36,6 +37,10 @@ describe('RoomUc', () => { provide: RoomMemberService, useValue: createMock(), }, + { + provide: ColumnBoardService, + useValue: createMock(), + }, { provide: AuthorizationService, useValue: createMock(), From 5592ec9cd7ebdc25ec19bfa4582106b476a42ae7 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Wed, 6 Nov 2024 13:49:13 +0100 Subject: [PATCH 17/20] fix linter error --- apps/server/src/modules/board/uc/board.uc.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/modules/board/uc/board.uc.spec.ts b/apps/server/src/modules/board/uc/board.uc.spec.ts index 7042425d7aa..e282cee6385 100644 --- a/apps/server/src/modules/board/uc/board.uc.spec.ts +++ b/apps/server/src/modules/board/uc/board.uc.spec.ts @@ -7,12 +7,12 @@ import { CourseRepo } from '@shared/repo'; import { setupEntities, userFactory } from '@shared/testing'; import { courseFactory } from '@shared/testing/factory'; import { LegacyLogger } from '@src/core/logger'; +import { RoomMemberService } from '@src/modules/room-member'; import { CopyElementType, CopyStatus, CopyStatusEnum } from '../../copy-helper'; import { BoardExternalReferenceType, BoardLayout, BoardNodeFactory, Column, ColumnBoard } from '../domain'; import { BoardNodePermissionService, BoardNodeService, ColumnBoardService } from '../service'; import { columnBoardFactory, columnFactory } from '../testing'; import { BoardUc } from './board.uc'; -import { RoomMemberService } from '@src/modules/room-member'; describe(BoardUc.name, () => { let module: TestingModule; From c8fc1dbdbd87705765d302a7a6ecd24e2a7e5ea9 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Thu, 7 Nov 2024 11:08:10 +0100 Subject: [PATCH 18/20] fix board context authorization --- apps/server/src/modules/board/uc/board.uc.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/server/src/modules/board/uc/board.uc.ts b/apps/server/src/modules/board/uc/board.uc.ts index 59552ff45d0..92e620f75e9 100644 --- a/apps/server/src/modules/board/uc/board.uc.ts +++ b/apps/server/src/modules/board/uc/board.uc.ts @@ -1,4 +1,4 @@ -import { Action, AuthorizationService } from '@modules/authorization'; +import { Action, AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { CopyStatus } from '@modules/copy-helper'; import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { Permission } from '@shared/domain/interface'; @@ -161,6 +161,10 @@ export class BoardUc { action: Action.write, requiredPermissions: [], }); + } else if (context.type === BoardExternalReferenceType.User) { + this.authorizationService.checkPermission(user, user, AuthorizationContextBuilder.write([])); + } else { + throw new Error(`Unsupported context type ${context.type as string}`); } } } From 4a7515f428abb32f1f7eead9d4d2cf48cd566622 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Fri, 8 Nov 2024 11:47:33 +0100 Subject: [PATCH 19/20] update uc parent permission check --- apps/server/src/modules/board/uc/board.uc.spec.ts | 14 ++++++++++++++ apps/server/src/modules/board/uc/board.uc.ts | 2 -- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/apps/server/src/modules/board/uc/board.uc.spec.ts b/apps/server/src/modules/board/uc/board.uc.spec.ts index e282cee6385..84e64021289 100644 --- a/apps/server/src/modules/board/uc/board.uc.spec.ts +++ b/apps/server/src/modules/board/uc/board.uc.spec.ts @@ -193,6 +193,20 @@ describe(BoardUc.name, () => { expect(result).toEqual(board); }); + describe('when context type is not supported', () => { + it('should throw an error', async () => { + const { user } = setup(); + + await expect( + uc.createBoard(user.id, { + title: 'new board', + layout: BoardLayout.COLUMNS, + parentId: new ObjectId().toHexString(), + parentType: BoardExternalReferenceType.User, + }) + ).rejects.toThrowError('Unsupported context type user'); + }); + }); }); }); diff --git a/apps/server/src/modules/board/uc/board.uc.ts b/apps/server/src/modules/board/uc/board.uc.ts index 92e620f75e9..aca3e1e10ed 100644 --- a/apps/server/src/modules/board/uc/board.uc.ts +++ b/apps/server/src/modules/board/uc/board.uc.ts @@ -161,8 +161,6 @@ export class BoardUc { action: Action.write, requiredPermissions: [], }); - } else if (context.type === BoardExternalReferenceType.User) { - this.authorizationService.checkPermission(user, user, AuthorizationContextBuilder.write([])); } else { throw new Error(`Unsupported context type ${context.type as string}`); } From b3033bf9a9654a25c40c803c38ba82f6e98a1378 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Fri, 8 Nov 2024 13:46:21 +0100 Subject: [PATCH 20/20] fix linter error --- apps/server/src/modules/board/uc/board.uc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/modules/board/uc/board.uc.ts b/apps/server/src/modules/board/uc/board.uc.ts index aca3e1e10ed..6a766370f74 100644 --- a/apps/server/src/modules/board/uc/board.uc.ts +++ b/apps/server/src/modules/board/uc/board.uc.ts @@ -1,4 +1,4 @@ -import { Action, AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { Action, AuthorizationService } from '@modules/authorization'; import { CopyStatus } from '@modules/copy-helper'; import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { Permission } from '@shared/domain/interface';