From 91abf0a1f7c6271f85f715fd928b3e47718ec9b3 Mon Sep 17 00:00:00 2001 From: Shun Usami Date: Thu, 28 Dec 2023 01:28:36 +0900 Subject: [PATCH] Feat/backend/direct message tests (#179) * [backend] Add direct room tests for `GET /rooms/:roomId` API * [backend] Modify test descriptions for `POST /rooms/:roomId/invite/:userId` * [backend] Add direct room tests for `GET /rooms` API * [backend] Add direct room tests for `PATCH /rooms/:roomId` * [backend] Fix UpdateRoomGuard for DIRECT room - Add exhaustive check for existing/updating accessLevel * [backend] Add direct room tests for `DELETE /room/:id` * [backend] Set default role to OWNER for DIRECT rooms so that both users in the room can delete the room * [backend] Add direct room tests for leave and kick * [backend] Add direct room tests for `PATCH /room/:id/:userId` API --- backend/src/room/guards/update-room.guard.ts | 68 ++-- backend/src/room/room.service.ts | 11 +- backend/test/room.e2e-spec.ts | 313 ++++++++++++++++--- 3 files changed, 329 insertions(+), 63 deletions(-) diff --git a/backend/src/room/guards/update-room.guard.ts b/backend/src/room/guards/update-room.guard.ts index 24533409..201e87b3 100644 --- a/backend/src/room/guards/update-room.guard.ts +++ b/backend/src/room/guards/update-room.guard.ts @@ -27,26 +27,54 @@ export class UpdateRoomGuard implements CanActivate { } const room = await this.roomService.findRoom(Number(roomId)); const dto: UpdateRoomDto = req.body; - // Remove password from PROTECTED by changing accessLevel to PUBLIC/PRIVATE is ok - if ( - room.accessLevel === 'PROTECTED' && - dto.accessLevel !== 'PROTECTED' && - !dto.password - ) { - return true; + switch (room.accessLevel) { + case 'PUBLIC': + case 'PRIVATE': + switch (dto.accessLevel) { + case 'PUBLIC': + case 'PRIVATE': + case undefined: + if (dto.password) { + throw new BadRequestException( + 'cannot set password for PUBLIC/PRIVATE room', + ); + } + return true; + case 'PROTECTED': + if (!dto.password) { + throw new BadRequestException('password is required'); + } + return true; + case 'DIRECT': + throw new BadRequestException('cannot update to DIRECT'); + default: + throw new BadRequestException('unreachable'); + } + case 'PROTECTED': + switch (dto.accessLevel) { + case 'PUBLIC': + case 'PRIVATE': + if (dto.password) { + throw new BadRequestException( + 'cannot set password for PUBLIC/PRIVATE room', + ); + } + return true; + case 'PROTECTED': + case undefined: + if (dto.password === null || dto.password === '') { + throw new BadRequestException('password cannot be empty'); + } + return true; + case 'DIRECT': + throw new BadRequestException('cannot update to DIRECT'); + default: + throw new BadRequestException('unreachable'); + } + case 'DIRECT': + throw new BadRequestException('cannot update DIRECT room'); + default: + throw new BadRequestException('unreachable'); } - - const updated = { ...room, ...dto }; - // non-PROTECTED room must not have password - if (updated.accessLevel !== 'PROTECTED' && updated.password) { - throw new BadRequestException( - 'password is only allowed for PROTECTED rooms', - ); - } - // PROTECTED room must have password - if (updated.accessLevel === 'PROTECTED' && !updated.password) { - throw new BadRequestException('password is required'); - } - return true; } } diff --git a/backend/src/room/room.service.ts b/backend/src/room/room.service.ts index c0ed0654..6068969d 100644 --- a/backend/src/room/room.service.ts +++ b/backend/src/room/room.service.ts @@ -31,6 +31,9 @@ export class RoomService { if (createRoomDto.accessLevel === 'DIRECT' && userIds.length !== 1) { throw new BadRequestException('Direct room should have only one user'); } + // If accessLevel is DIRECT, set defaultRole to OWNER + const defaultRole = + createRoomDto.accessLevel === 'DIRECT' ? Role.OWNER : Role.MEMBER; const room = await this.prisma.room.create({ data: { @@ -43,7 +46,7 @@ export class RoomService { }, ...userIds.map((userId) => ({ userId: userId, - role: Role.MEMBER, + role: defaultRole, })), ], }, @@ -205,6 +208,12 @@ export class RoomService { }; async kickUser(roomId: number, userId: number): Promise { + const room = await this.prisma.room.findUniqueOrThrow({ + where: { id: roomId }, + }); + if (room.accessLevel === 'DIRECT') { + throw new ForbiddenException('Direct room cannot kick/leave user'); + } const deletedUserOnRoom = await this.prisma.userOnRoom.delete({ where: { userId_roomId_unique: { diff --git a/backend/test/room.e2e-spec.ts b/backend/test/room.e2e-spec.ts index fd5eff92..96ab10e8 100644 --- a/backend/test/room.e2e-spec.ts +++ b/backend/test/room.e2e-spec.ts @@ -267,6 +267,9 @@ describe('RoomController (e2e)', () => { it('should get private room', () => { return app.getRoom(privateRoom.id, owner.accessToken).expect(200); }); + it('should not get direct room (403 Forbidden)', () => { + return app.getRoom(directRoom.id, owner.accessToken).expect(403); + }); }); describe('admin', () => { @@ -279,6 +282,9 @@ describe('RoomController (e2e)', () => { it('should get private room', () => { return app.getRoom(privateRoom.id, admin.accessToken).expect(200); }); + it('should not get direct room (403 Forbidden)', () => { + return app.getRoom(directRoom.id, admin.accessToken).expect(403); + }); }); describe('member', () => { @@ -291,6 +297,9 @@ describe('RoomController (e2e)', () => { it('should get private room', () => { return app.getRoom(privateRoom.id, member.accessToken).expect(200); }); + it('should not get direct room (403 Forbidden)', () => { + return app.getRoom(directRoom.id, member.accessToken).expect(403); + }); }); describe('notMember', () => { @@ -303,13 +312,22 @@ describe('RoomController (e2e)', () => { it('should not get private room', () => { return app.getRoom(privateRoom.id, notMember.accessToken).expect(403); }); + it('should not get direct room (403 Forbidden)', () => { + return app.getRoom(directRoom.id, notMember.accessToken).expect(403); + }); }); - it('public room should be accessed by notMember (200 OK)', async () => {}); - - it('private room should not be accessed by notMember (403 Forbidden)', async () => {}); + describe('user1', () => { + it('should get direct room (200 OK)', () => { + return app.getRoom(directRoom.id, user1.accessToken).expect(200); + }); + }); - it('protected room should not be accessed by notMember (403 Forbidden)', async () => {}); + describe('user2', () => { + it('should get direct room (200 OK)', () => { + return app.getRoom(directRoom.id, user2.accessToken).expect(200); + }); + }); it('invalid roomId should return 404 Not Found (403?)', async () => {}); }); @@ -535,12 +553,12 @@ describe('RoomController (e2e)', () => { }); }); describe('DIRECT ROOM', () => { - it('should not invite anyone (403 Forbidden)', async () => { + test('user1 should not invite anyone (403 Forbidden)', async () => { await app .inviteRoom(directRoom.id, notMember.id, user1.accessToken) .expect(403); }); - it('should not invite anyone (403 Forbidden)', async () => { + test('user2 should not invite anyone (403 Forbidden)', async () => { await app .inviteRoom(directRoom.id, notMember.id, user2.accessToken) .expect(403); @@ -560,33 +578,81 @@ describe('RoomController (e2e)', () => { expect(res.body).toContainEqual(protectedRoom); }); describe('owner', () => { - it('should get all rooms (200 OK)', async () => { - await testGetRooms(owner.accessToken).expect((res) => { - expect(res.body).toContainEqual(privateRoom); - }); + let _rooms: RoomEntity[]; + it('should get rooms (200 OK)', async () => { + _rooms = await testGetRooms(owner.accessToken).then((res) => res.body); + }); + it('should contain private room', async () => { + expect(_rooms).toContainEqual(privateRoom); + }); + it('should not contain the direct room', async () => { + _rooms.forEach((room) => expect(room).not.toEqual(directRoom)); }); }); describe('admin', () => { - it('should get all rooms (200 OK)', async () => { - await testGetRooms(admin.accessToken).expect((res) => { - expect(res.body).toContainEqual(privateRoom); - }); + let _rooms: RoomEntity[]; + it('should get rooms (200 OK)', async () => { + _rooms = await testGetRooms(admin.accessToken).then((res) => res.body); + }); + it('should contain private room', async () => { + expect(_rooms).toContainEqual(privateRoom); + }); + it('should not contain the direct room', async () => { + _rooms.forEach((room) => expect(room).not.toEqual(directRoom)); }); }); describe('member', () => { - it('should get all rooms (200 OK)', async () => { - await testGetRooms(member.accessToken).expect((res) => { - expect(res.body).toContainEqual(privateRoom); - }); + let _rooms: RoomEntity[]; + it('should get rooms (200 OK)', async () => { + _rooms = await testGetRooms(member.accessToken).then((res) => res.body); + }); + it('should contain private room', async () => { + expect(_rooms).toContainEqual(privateRoom); + }); + it('should not contain the direct room', async () => { + _rooms.forEach((room) => expect(room).not.toEqual(directRoom)); }); }); describe('non-member', () => { - it('should not get private rooms (200 OK)', async () => { - await testGetRooms(notMember.accessToken).expect((res) => { - const expectNotPrivate = (room: RoomEntity) => - expect(room.accessLevel).not.toEqual('PRIVATE'); - res.body.forEach(expectNotPrivate); - }); + let _rooms: RoomEntity[]; + it('should get rooms (200 OK)', async () => { + _rooms = await testGetRooms(notMember.accessToken).then( + (res) => res.body, + ); + }); + it('should not contain private room', async () => { + _rooms.forEach((room) => expect(room).not.toEqual(privateRoom)); + }); + it('should not contain the direct room', async () => { + _rooms.forEach((room) => expect(room).not.toEqual(directRoom)); + }); + }); + describe('user1', () => { + let _rooms: RoomEntity[]; + it('should get all rooms (200 OK)', async () => { + _rooms = await testGetRooms(user1.accessToken).then((res) => res.body); + }); + it('should contain direct room', async () => { + expect(_rooms).toContainEqual(directRoom); + }); + it('should not contain private room', async () => { + _rooms.forEach((room) => + expect(room.accessLevel).not.toEqual('PRIVATE'), + ); + }); + }); + describe('user2', () => { + let _rooms: RoomEntity[]; + it('should get all rooms (200 OK)', async () => { + _rooms = await testGetRooms(user2.accessToken).then((res) => res.body); + }); + it('should contain direct room', async () => { + expect(_rooms).toContainEqual(directRoom); + }); + it('should not contain private room', async () => { + _rooms.forEach((room) => + expect(room.accessLevel).not.toEqual('PRIVATE'), + ); }); }); }); @@ -668,6 +734,9 @@ describe('RoomController (e2e)', () => { await update({ password: '12345678' }).expect(400); }); } + it('should not update access_level to direct (400 Bad Request)', async () => { + await update({ accessLevel: 'DIRECT' }).expect(400); + }); }); describe('admin', shouldNotUpdateAny(updater(adminRef, _roomRef))); describe('member', shouldNotUpdateAny(updater(memberRef, _roomRef))); @@ -680,96 +749,174 @@ describe('RoomController (e2e)', () => { describe('PUBLIC room', testUpdateRoom(constants.room.publicRoom)); describe('PRIVATE room', testUpdateRoom(constants.room.privateRoom)); describe('PROTECTED room', testUpdateRoom(constants.room.protectedRoom)); + describe('DIRECT room', () => { + let _room: RoomEntity; + beforeEach(async () => { + _room = await app + .createRoom( + { ...constants.room.directRoom, userIds: [user2.id] }, + user1.accessToken, + ) + .then((res) => res.body); + }); + afterEach(async () => { + await app.deleteRoom(_room.id, user1.accessToken); + }); + it('should not update name (400 Forbidden)', async () => { + await app + .updateRoom(_room.id, { name: 'new_name' }, user1.accessToken) + .expect(400); + }); + it('should not update access_level (400 Forbidden)', async () => { + await app + .updateRoom(_room.id, { accessLevel: 'PUBLIC' }, user1.accessToken) + .expect(400); + await app + .updateRoom(_room.id, { accessLevel: 'PRIVATE' }, user1.accessToken) + .expect(400); + await app + .updateRoom( + _room.id, + { accessLevel: 'PROTECTED', password: '12345678' }, + user1.accessToken, + ) + .expect(400); + }); + }); it('invalid roomId should return 404 Not Found', async () => {}); }); describe('DELETE /room/:id (Delete Room)', () => { - let _publicRoom, _privateRoom, _protectedRoom: RoomEntity; + let _publicRoom, _privateRoom, _protectedRoom, _directRoom: RoomEntity; const setupRooms = async () => { _publicRoom = await setupRoom(constants.room.publicRoom); _privateRoom = await setupRoom(constants.room.privateRoom); _protectedRoom = await setupRoom(constants.room.protectedRoom); + _directRoom = await app + .createRoom( + { ...constants.room.directRoom, userIds: [user2.id] }, + user1.accessToken, + ) + .expect(201) + .then((res) => res.body); }; const teardownRooms = async () => { await app.deleteRoom(_publicRoom.id, owner.accessToken); await app.deleteRoom(_privateRoom.id, owner.accessToken); await app.deleteRoom(_protectedRoom.id, owner.accessToken); + await app.deleteRoom(_directRoom.id, user1.accessToken); }; describe('owner', () => { beforeAll(setupRooms); afterAll(teardownRooms); - test('should delete public room (204 No Content)', async () => { + it('should delete public room (204 No Content)', async () => { await app.deleteRoom(_publicRoom.id, owner.accessToken).expect(204); }); - test('should delete private room (204 No Content)', async () => { + it('should delete private room (204 No Content)', async () => { await app.deleteRoom(_privateRoom.id, owner.accessToken).expect(204); }); - test('should delete protected room (204 No Content)', async () => { + it('should delete protected room (204 No Content)', async () => { await app.deleteRoom(_protectedRoom.id, owner.accessToken).expect(204); }); + it('should not delete direct room (403 Forbidden)', async () => { + await app.deleteRoom(_directRoom.id, owner.accessToken).expect(403); + }); }); describe('admin', () => { beforeAll(setupRooms); afterAll(teardownRooms); - test('should not delete public room (403 Forbidden)', async () => { + it('should not delete public room (403 Forbidden)', async () => { await app.deleteRoom(_publicRoom.id, admin.accessToken).expect(403); }); - test('should not delete private room (403 Forbidden)', async () => { + it('should not delete private room (403 Forbidden)', async () => { await app.deleteRoom(_privateRoom.id, admin.accessToken).expect(403); }); - test('should not delete protected room (403 Forbidden)', async () => { + it('should not delete protected room (403 Forbidden)', async () => { await app.deleteRoom(_protectedRoom.id, admin.accessToken).expect(403); }); + it('should not delete direct room (403 Forbidden)', async () => { + await app.deleteRoom(_directRoom.id, admin.accessToken).expect(403); + }); }); describe('member', () => { beforeAll(setupRooms); afterAll(teardownRooms); - test('should not delete public room (403 Forbidden)', async () => { + it('should not delete public room (403 Forbidden)', async () => { await app.deleteRoom(_publicRoom.id, member.accessToken).expect(403); }); - test('should not delete private room (403 Forbidden)', async () => { + it('should not delete private room (403 Forbidden)', async () => { await app.deleteRoom(_privateRoom.id, member.accessToken).expect(403); }); - test('should not delete protected room (403 Forbidden)', async () => { + it('should not delete protected room (403 Forbidden)', async () => { await app.deleteRoom(_protectedRoom.id, member.accessToken).expect(403); }); + it('should not delete direct room (403 Forbidden)', async () => { + await app.deleteRoom(_directRoom.id, member.accessToken).expect(403); + }); }); describe('non-member', () => { beforeAll(setupRooms); afterAll(teardownRooms); - test('should not delete public room (403 Forbidden)', async () => { + it('should not delete public room (403 Forbidden)', async () => { await app.deleteRoom(_publicRoom.id, notMember.accessToken).expect(403); }); - test('should not delete private room (403 Forbidden)', async () => { + it('should not delete private room (403 Forbidden)', async () => { await app .deleteRoom(_privateRoom.id, notMember.accessToken) .expect(403); }); - test('should not delete protected room (403 Forbidden)', async () => { + it('should not delete protected room (403 Forbidden)', async () => { await app .deleteRoom(_protectedRoom.id, notMember.accessToken) .expect(403); }); + it('should not delete direct room (403 Forbidden)', async () => { + await app.deleteRoom(_directRoom.id, notMember.accessToken).expect(403); + }); + }); + + describe('user1', () => { + beforeAll(setupRooms); + afterAll(teardownRooms); + it('should delete direct room (204 No Content)', async () => { + await app.deleteRoom(_directRoom.id, user1.accessToken).expect(204); + }); + }); + + describe('user2', () => { + beforeAll(setupRooms); + afterAll(teardownRooms); + it('should delete direct room (204 No Content)', async () => { + await app.deleteRoom(_directRoom.id, user2.accessToken).expect(204); + }); }); it('invalid roomId should return 404 Not Found', async () => {}); }); describe('DELETE /room/:id/:userId (Leave)', () => { - let _publicRoom, _privateRoom, _protectedRoom: RoomEntity; + let _publicRoom, _privateRoom, _protectedRoom, _directRoom: RoomEntity; const setupRooms = async () => { _publicRoom = await setupRoom(constants.room.publicRoom); _privateRoom = await setupRoom(constants.room.privateRoom); _protectedRoom = await setupRoom(constants.room.protectedRoom); + _directRoom = await app + .createRoom( + { ...constants.room.directRoom, userIds: [user2.id] }, + user1.accessToken, + ) + .expect(201) + .then((res) => res.body); }; const teardownRooms = async () => { await app.deleteRoom(_publicRoom.id, owner.accessToken); await app.deleteRoom(_privateRoom.id, owner.accessToken); await app.deleteRoom(_protectedRoom.id, owner.accessToken); + await app.deleteRoom(_directRoom.id, user1.accessToken); }; describe('owner', () => { beforeAll(setupRooms); @@ -798,7 +945,6 @@ describe('RoomController (e2e)', () => { await app.leaveRoom(_protectedRoom.id, admin.accessToken).expect(204); }); }); - describe('member', () => { beforeAll(setupRooms); afterAll(teardownRooms); @@ -812,7 +958,6 @@ describe('RoomController (e2e)', () => { await app.leaveRoom(_protectedRoom.id, member.accessToken).expect(204); }); }); - describe('non-member', () => { beforeAll(setupRooms); afterAll(teardownRooms); @@ -828,19 +973,41 @@ describe('RoomController (e2e)', () => { .expect(403); }); }); + describe('user1', () => { + beforeAll(setupRooms); + afterAll(teardownRooms); + test('should not leave direct room (403 Forbidden)', async () => { + await app.leaveRoom(directRoom.id, user1.accessToken).expect(403); + }); + }); + describe('user2', () => { + beforeAll(setupRooms); + afterAll(teardownRooms); + test('should not leave direct room (403 Forbidden)', async () => { + await app.leaveRoom(directRoom.id, user2.accessToken).expect(403); + }); + }); }); describe('DELETE /room/:id/kick/:userId (Kick)', () => { - let _publicRoom, _privateRoom, _protectedRoom: RoomEntity; + let _publicRoom, _privateRoom, _protectedRoom, _directRoom: RoomEntity; const setupRooms = async () => { _publicRoom = await setupRoom(constants.room.publicRoom); _privateRoom = await setupRoom(constants.room.privateRoom); _protectedRoom = await setupRoom(constants.room.protectedRoom); + _directRoom = await app + .createRoom( + { ...constants.room.directRoom, userIds: [user2.id] }, + user1.accessToken, + ) + .expect(201) + .then((res) => res.body); }; const teardownRooms = async () => { await app.deleteRoom(_publicRoom.id, owner.accessToken); await app.deleteRoom(_privateRoom.id, owner.accessToken); await app.deleteRoom(_protectedRoom.id, owner.accessToken); + await app.deleteRoom(_directRoom.id, user1.accessToken); }; const kickAll = async ( userId: number, @@ -919,19 +1086,43 @@ describe('RoomController (e2e)', () => { await kickAll(notMember.id, notMember.accessToken, 403); }); }); + describe('user1', () => { + beforeEach(setupRooms); + afterEach(teardownRooms); + it('should not kick anyone (403 Forbidden)', async () => { + await kickAll(user1.id, user1.accessToken, 403); + await kickAll(user2.id, user1.accessToken, 403); + }); + }); + describe('user2', () => { + beforeEach(setupRooms); + afterEach(teardownRooms); + it('should not kick anyone (403 Forbidden)', async () => { + await kickAll(user1.id, user2.accessToken, 403); + await kickAll(user2.id, user2.accessToken, 403); + }); + }); }); describe('PATCH /room/:id/:userId (Update Role)', () => { - let _publicRoom, _privateRoom, _protectedRoom: RoomEntity; + let _publicRoom, _privateRoom, _protectedRoom, _directRoom: RoomEntity; const setupRooms = async () => { _publicRoom = await setupRoom(constants.room.publicRoom); _privateRoom = await setupRoom(constants.room.privateRoom); _protectedRoom = await setupRoom(constants.room.protectedRoom); + _directRoom = await app + .createRoom( + { ...constants.room.directRoom, userIds: [user2.id] }, + user1.accessToken, + ) + .expect(201) + .then((res) => res.body); }; const teardownRooms = async () => { await app.deleteRoom(_publicRoom.id, owner.accessToken); await app.deleteRoom(_privateRoom.id, owner.accessToken); await app.deleteRoom(_protectedRoom.id, owner.accessToken); + await app.deleteRoom(_directRoom.id, user1.accessToken); }; beforeEach(setupRooms); afterEach(teardownRooms); @@ -1236,6 +1427,44 @@ describe('RoomController (e2e)', () => { .expect(403); }); }); + describe('Target: User1', () => { + it('should not update role to member (403 Forbidden)', async () => { + await app + .updateUserOnRoom( + _directRoom.id, + user1.id, + { role: Role.MEMBER }, + notMember.accessToken, + ) + .expect(403); + }); + }); + }); + describe('User1', () => { + describe('Target: User1', () => { + it('should not update role to member (403 Forbidden)', async () => { + await app + .updateUserOnRoom( + _directRoom.id, + user1.id, + { role: Role.MEMBER }, + user1.accessToken, + ) + .expect(403); + }); + }); + describe('Target: User2', () => { + it('should not update role to member (403 Forbidden)', async () => { + await app + .updateUserOnRoom( + _directRoom.id, + user2.id, + { role: Role.MEMBER }, + user1.accessToken, + ) + .expect(403); + }); + }); }); });