Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BC-8348 - Implement Sharing and Copying of Room Boards #5338

Closed
wants to merge 71 commits into from
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
a6db175
add schoolId to room
uidp Nov 11, 2024
b320a19
rename room roles - remove underscore
uidp Nov 11, 2024
b290e4a
fix test
uidp Nov 12, 2024
6198f50
rename school field
uidp Nov 12, 2024
834a059
fix entity fields
uidp Nov 12, 2024
4066cd5
update migrations seed
uidp Nov 12, 2024
902d21e
add ROOM_CREATE and ROOM_DELETE permissions
uidp Nov 13, 2024
0c9b430
add migration to infer room school from room memberships
uidp Nov 13, 2024
7c47ef7
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
uidp Nov 14, 2024
22a2224
update seeds
uidp Nov 14, 2024
f5340a5
extend api to return room permissions
uidp Nov 14, 2024
fa3d01e
refactor board copy to using room storage id
uidp Nov 15, 2024
5dc2227
remove comment
uidp Nov 15, 2024
adfe30a
Merge branch 'main' into BC-8348-copy-share-board
uidp Nov 15, 2024
a50cfd0
simplify destination reference for sahre token import
uidp Nov 17, 2024
ebb15e2
Merge branch 'BC-8348-copy-share-board' of github.com:hpi-schul-cloud…
uidp Nov 17, 2024
c776afa
refactor share import destination
uidp Nov 19, 2024
b430522
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
uidp Nov 19, 2024
c26af42
rename room role enum keys
uidp Nov 19, 2024
c716fdd
add missing module import
uidp Nov 19, 2024
b26f9e1
adapt share token create and lookup auth
uidp Nov 25, 2024
eb7ba3e
fix unit test
uidp Nov 25, 2024
f7080d2
fix tests
uidp Nov 25, 2024
0a0783a
adapt token lookup permission
uidp Nov 25, 2024
d8fb524
fix room tests
uidp Nov 25, 2024
7f6221e
fix board tests
uidp Nov 25, 2024
76c3843
fix board tests
uidp Nov 25, 2024
bd25687
fix board tests
uidp Nov 25, 2024
0dbe7cc
fix board tests
uidp Nov 25, 2024
4d602f0
fix board tests
uidp Nov 25, 2024
7543526
fix board tests
uidp Nov 26, 2024
62d3b00
fix board tests
uidp Nov 26, 2024
7465d5a
fix board tests
uidp Nov 26, 2024
97612fc
fix board tests
uidp Nov 26, 2024
cf6c434
fix board tests
uidp Nov 26, 2024
871e8e4
fix board tests
uidp Nov 26, 2024
ffbb39c
fix board tests
uidp Nov 26, 2024
871e37b
fix board tests
uidp Nov 26, 2024
15e59c6
fix board tests
uidp Nov 26, 2024
02e17b2
Merge branch 'main' into BC-8348-copy-share-board
uidp Nov 26, 2024
574ed91
fix board tests
uidp Nov 26, 2024
a798db6
Merge branch 'BC-8348-copy-share-board' of github.com:hpi-schul-cloud…
uidp Nov 26, 2024
ea66c6f
fix room-member tests
uidp Nov 26, 2024
285a465
fix board-copy-service.spec
uidp Nov 27, 2024
aec3e57
fix dependency cycle
uidp Nov 27, 2024
fc6fdd9
reset sharing test changes
uidp Nov 27, 2024
a8507c3
reset room test changes
uidp Nov 27, 2024
62d7bc2
reset room-member test changes
uidp Nov 27, 2024
df9704d
reset board test changes
uidp Nov 27, 2024
aed45b4
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
uidp Nov 27, 2024
6896568
implement draft status on board copy
uidp Nov 27, 2024
8b9feea
rename room-member to room-membership
uidp Nov 27, 2024
9fb359d
fix linter errors
uidp Nov 28, 2024
84a0158
rename variables regarding roomMembership
uidp Nov 28, 2024
c887a77
add schoolId to roomMembership
uidp Nov 28, 2024
c23014b
add api test
uidp Nov 28, 2024
dc66818
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
uidp Nov 28, 2024
82f2924
fix migration seeds
uidp Nov 28, 2024
7d4f69a
undo rename migration
uidp Dec 2, 2024
939ae36
undo rename migration
uidp Dec 2, 2024
2437608
rename comments
uidp Dec 2, 2024
cd2a197
rename comments and console outputs
uidp Dec 2, 2024
097b117
fix module imports
uidp Dec 2, 2024
82c4c30
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
uidp Dec 2, 2024
3993226
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
uidp Dec 2, 2024
8f5e6cd
implement handling optional date values
uidp Dec 3, 2024
4a303ed
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
uidp Dec 3, 2024
0b47cd3
add api tests
uidp Dec 3, 2024
09eee1b
fix tests and linting
uidp Dec 3, 2024
ec854b1
fix test coverage
uidp Dec 3, 2024
1d6d7fa
add api tests
uidp Dec 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { EntityManager } from '@mikro-orm/mongodb';
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { Permission, RoleName } from '@shared/domain/interface';
import {
cleanupCollections,
groupEntityFactory,
roleFactory,
TestApiClient,
UserAndAccountTestFactory,
} from '@shared/testing';
import { GroupEntityTypes } from '@src/modules/group/entity';
import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing';
import { roomEntityFactory } from '@src/modules/room/testing';
import { ServerTestModule } from '@src/modules/server';
import { BoardExternalReferenceType, ColumnBoardProps } 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);
});

describe('with valid user', () => {
const setup = async (columnBoardProps: Partial<ColumnBoardProps> = {}) => {
const room = roomEntityFactory.buildWithId();
const role = roleFactory.buildWithId({
name: RoleName.ROOMEDITOR,
permissions: [Permission.ROOM_EDIT],
});
const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher();
const userGroup = groupEntityFactory.buildWithId({
type: GroupEntityTypes.ROOM,
users: [{ role, user: teacherUser }],
});
const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id });
const columnBoardNode = columnBoardEntityFactory.build({
...columnBoardProps,
context: { id: room.id, type: BoardExternalReferenceType.Room },
});
await em.persistAndFlush([room, roomMembership, teacherAccount, teacherUser, userGroup, role, columnBoardNode]);
em.clear();

const loggedInClient = await testApiClient.login(teacherAccount);

return { loggedInClient, columnBoardNode };
};

it('should return status 201', async () => {
const { loggedInClient, columnBoardNode } = await setup();

const response = await loggedInClient.post(`${columnBoardNode.id}/copy`);

expect(response.status).toEqual(201);
});
});
});
4 changes: 2 additions & 2 deletions apps/server/src/modules/board/uc/board.uc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export class BoardUc {

return { id: room.schoolId, type: StorageLocation.SCHOOL };
}

throw new Error(`Cannot get storage location reference for context type ${context.type as string}`);
/* istanbul ignore next */
uidp marked this conversation as resolved.
Show resolved Hide resolved
throw new Error(`Unsupported board reference type ${context.type as string}`);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { SanitizeHtml } from '@shared/controller';
import { NullToUndefined, SanitizeHtml } from '@shared/controller';
import { RoomCreateProps } from '@modules/room/domain';
import { RoomColor } from '@modules/room/domain/type';
import { IsDate, IsEnum, IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
Expand All @@ -23,17 +23,19 @@ export class CreateRoomBodyParams implements Omit<RoomCreateProps, 'schoolId'> {
@IsEnum(RoomColor)
color!: RoomColor;

@IsDate()
@IsOptional()
@NullToUndefined()
@IsDate()
@ApiPropertyOptional({
description: 'Start date of the room',
required: false,
type: Date,
})
startDate?: Date;

@IsDate()
@IsOptional()
@NullToUndefined()
@IsDate()
@ApiPropertyOptional({
description: 'End date of the room',
required: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { SanitizeHtml } from '@shared/controller';
import { RoomUpdateProps } from '@modules/room/domain';
import { RoomColor } from '@modules/room/domain/type';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { NullToUndefined, SanitizeHtml } from '@shared/controller';
import { IsDate, IsEnum, IsOptional, IsString, MaxLength, MinLength } from 'class-validator';

export class UpdateRoomBodyParams implements RoomUpdateProps {
Expand All @@ -25,6 +25,7 @@ export class UpdateRoomBodyParams implements RoomUpdateProps {

@IsDate()
@IsOptional()
@NullToUndefined()
@ApiPropertyOptional({
description: 'Start date of the room',
required: false,
Expand All @@ -34,6 +35,7 @@ export class UpdateRoomBodyParams implements RoomUpdateProps {

@IsDate()
@IsOptional()
@NullToUndefined()
@ApiPropertyOptional({
description: 'Start date of the room',
required: false,
Expand Down
5 changes: 3 additions & 2 deletions apps/server/src/modules/room/api/room.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Param,
Patch,
Post,
Put,
Query,
UnauthorizedException,
} from '@nestjs/common';
Expand Down Expand Up @@ -115,8 +116,8 @@ export class RoomController {
return response;
}

@Patch(':roomId')
@ApiOperation({ summary: 'Create a new room' })
@Put(':roomId')
@ApiOperation({ summary: 'Update an existing room' })
@ApiResponse({ status: HttpStatus.OK, description: 'Returns the details of a room', type: RoomDetailsResponse })
@ApiResponse({ status: HttpStatus.BAD_REQUEST, type: ApiValidationError })
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, type: UnauthorizedException })
Expand Down
81 changes: 53 additions & 28 deletions apps/server/src/modules/room/api/test/room-update.api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ describe('Room Controller (API)', () => {
await app.close();
});

describe('PATCH /rooms/:id', () => {
describe('PUT /rooms/:id', () => {
describe('when the user is not authenticated', () => {
it('should return a 401 error', async () => {
const someId = new ObjectId().toHexString();
const response = await testApiClient.patch(someId);
const response = await testApiClient.put(someId);
expect(response.status).toBe(HttpStatus.UNAUTHORIZED);
});
});
Expand All @@ -68,7 +68,7 @@ describe('Room Controller (API)', () => {
const { loggedInClient } = await setup();
const someId = new ObjectId().toHexString();
const params = { name: 'Room #101', color: 'green' };
const response = await loggedInClient.patch(someId, params);
const response = await loggedInClient.put(someId, params);
expect(response.status).toBe(HttpStatus.FORBIDDEN);
});
});
Expand All @@ -87,7 +87,7 @@ describe('Room Controller (API)', () => {
it('should return a 400 error', async () => {
const { loggedInClient } = await setup();
const params = { name: 'Room #101', color: 'green' };
const response = await loggedInClient.patch('42', params);
const response = await loggedInClient.put('42', params);
expect(response.status).toBe(HttpStatus.BAD_REQUEST);
});
});
Expand Down Expand Up @@ -121,7 +121,7 @@ describe('Room Controller (API)', () => {
const someId = new ObjectId().toHexString();
const params = { name: 'Room #101', color: 'green' };

const response = await loggedInClient.patch(someId, params);
const response = await loggedInClient.put(someId, params);

expect(response.status).toBe(HttpStatus.NOT_FOUND);
});
Expand All @@ -132,7 +132,7 @@ describe('Room Controller (API)', () => {
const { loggedInClient, room } = await setup();
const params = { name: 'Room #101', color: 'green' };

const response = await loggedInClient.patch(room.id, params);
const response = await loggedInClient.put(room.id, params);

expect(response.status).toBe(HttpStatus.OK);
await expect(em.findOneOrFail(RoomEntity, room.id)).resolves.toMatchObject({
Expand All @@ -147,7 +147,7 @@ describe('Room Controller (API)', () => {
const { loggedInClient, room } = await setup();
const params = { name: '', color: 'red' };

const response = await loggedInClient.patch(room.id, params);
const response = await loggedInClient.put(room.id, params);

expect(response.status).toBe(HttpStatus.BAD_REQUEST);
});
Expand All @@ -158,7 +158,7 @@ describe('Room Controller (API)', () => {
const { loggedInClient, room } = await setup();
const params = { name: 'Room #101', color: '' };

const response = await loggedInClient.patch(room.id, params);
const response = await loggedInClient.put(room.id, params);

expect(response.status).toBe(HttpStatus.BAD_REQUEST);
});
Expand All @@ -169,7 +169,7 @@ describe('Room Controller (API)', () => {
const { loggedInClient, room } = await setup();
const params = { name: 'Room #101', color: 'fancy-color' };

const response = await loggedInClient.patch(room.id, params);
const response = await loggedInClient.put(room.id, params);

expect(response.status).toBe(HttpStatus.BAD_REQUEST);
});
Expand All @@ -181,7 +181,7 @@ describe('Room Controller (API)', () => {
const { loggedInClient, room } = await setup();

const params = { name: 'Room #101', color: 'green', startDate: '2024-10-02' };
const response = await loggedInClient.patch(room.id, params);
const response = await loggedInClient.put(room.id, params);

expect(response.status).toBe(HttpStatus.OK);
await expect(em.findOneOrFail(RoomEntity, room.id)).resolves.toMatchObject({
Expand All @@ -194,7 +194,7 @@ describe('Room Controller (API)', () => {
it('should return a 400 error', async () => {
const { loggedInClient, room } = await setup();
const params = { name: 'Room #101', color: 'green', startDate: 'invalid date' };
const response = await loggedInClient.patch(room.id, params);
const response = await loggedInClient.put(room.id, params);
expect(response.status).toBe(HttpStatus.BAD_REQUEST);
});
});
Expand All @@ -204,23 +204,35 @@ describe('Room Controller (API)', () => {
const { loggedInClient, room } = await setup();
const params = { name: 'Room #101', color: 'green', startDate: null };

const response = await loggedInClient.patch(room.id, params);
const response = await loggedInClient.put(room.id, params);

expect(response.status).toBe(HttpStatus.OK);
await expect(em.findOneOrFail(RoomEntity, room.id)).resolves.toMatchObject({
id: room.id,
startDate: null,
});
const resultRoom = await em.findOneOrFail(RoomEntity, room.id);
expect(resultRoom.startDate).toBe(undefined);
});
});
});

describe('when the startDate is omitted', () => {
it('should unset the property', async () => {
const { loggedInClient, room } = await setup();
const params = { name: 'Room #101', color: 'green' };

const response = await loggedInClient.put(room.id, params);

expect(response.status).toBe(HttpStatus.OK);

const resultRoom = await em.findOneOrFail(RoomEntity, room.id);
expect(resultRoom.endDate).toBe(undefined);
});
});

describe('when an end date is given', () => {
it('should update the room', async () => {
const { loggedInClient, room } = await setup();
const params = { name: 'Room #101', color: 'green', endDate: '2024-10-18' };

const response = await loggedInClient.patch(room.id, params);
const response = await loggedInClient.put(room.id, params);

expect(response.status).toBe(HttpStatus.OK);
await expect(em.findOneOrFail(RoomEntity, room.id)).resolves.toMatchObject({
Expand All @@ -233,27 +245,40 @@ describe('Room Controller (API)', () => {
it('should return a 400 error', async () => {
const { loggedInClient, room } = await setup();
const params = { name: 'Room #101', color: 'green', endDate: 'invalid date' };
const response = await loggedInClient.patch(room.id, params);
const response = await loggedInClient.put(room.id, params);
expect(response.status).toBe(HttpStatus.BAD_REQUEST);
});
});

describe('when the date is null', () => {
it('should unset the property', async () => {
const { loggedInClient, room } = await setup();
const params = { name: 'Room #101', color: 'green', endDate: null };
const params = { name: 'Room #101', color: 'green', startDate: '2024-10-02', endDate: null };

const response = await loggedInClient.patch(room.id, params);
const response = await loggedInClient.put(room.id, params);

expect(response.status).toBe(HttpStatus.OK);
await expect(em.findOneOrFail(RoomEntity, room.id)).resolves.toMatchObject({
id: room.id,
endDate: null,
});

const resultRoom = await em.findOneOrFail(RoomEntity, room.id);
expect(resultRoom.endDate).toBe(undefined);
});
});
});

describe('when the endDate is omitted', () => {
it('should unset the property', async () => {
const { loggedInClient, room } = await setup();
const params = { name: 'Room #101', color: 'green', startDate: '2024-10-02' };

const response = await loggedInClient.put(room.id, params);

expect(response.status).toBe(HttpStatus.OK);

const resultRoom = await em.findOneOrFail(RoomEntity, room.id);
expect(resultRoom.endDate).toBe(undefined);
});
});

describe('when the start date is before the end date', () => {
it('should update the room', async () => {
const { loggedInClient, room } = await setup();
Expand All @@ -264,7 +289,7 @@ describe('Room Controller (API)', () => {
endDate: '2024-10-18',
};

const response = await loggedInClient.patch(room.id, params);
const response = await loggedInClient.put(room.id, params);

expect(response.status).toBe(HttpStatus.OK);
await expect(em.findOneOrFail(RoomEntity, room.id)).resolves.toMatchObject({
Expand All @@ -285,7 +310,7 @@ describe('Room Controller (API)', () => {
endDate: '2024-10-05',
};

const response = await loggedInClient.patch(room.id, params);
const response = await loggedInClient.put(room.id, params);

expect(response.status).toBe(HttpStatus.BAD_REQUEST);
});
Expand Down Expand Up @@ -313,7 +338,7 @@ describe('Room Controller (API)', () => {
const someId = new ObjectId().toHexString();
const params = { name: 'Room #101', color: 'green' };

const response = await loggedInClient.patch(someId, params);
const response = await loggedInClient.put(someId, params);

expect(response.status).toBe(HttpStatus.NOT_FOUND);
});
Expand All @@ -324,7 +349,7 @@ describe('Room Controller (API)', () => {
const { loggedInClient, room } = await setup();
const params = { name: 'Room #101', color: 'green' };

const response = await loggedInClient.patch(room.id, params);
const response = await loggedInClient.put(room.id, params);

expect(response.status).toBe(HttpStatus.FORBIDDEN);
});
Expand Down
Loading
Loading