diff --git a/backend/prisma/migrations/20240118204714_add_mute_user_on_room_model/migration.sql b/backend/prisma/migrations/20240118204714_add_mute_user_on_room_model/migration.sql new file mode 100644 index 00000000..81bb99e0 --- /dev/null +++ b/backend/prisma/migrations/20240118204714_add_mute_user_on_room_model/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "MuteUserOnRoom" ( + "userId" INTEGER NOT NULL, + "roomId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expiresAt" TIMESTAMP(3) +); + +-- CreateIndex +CREATE UNIQUE INDEX "MuteUserOnRoom_userId_roomId_key" ON "MuteUserOnRoom"("userId", "roomId"); + +-- AddForeignKey +ALTER TABLE "MuteUserOnRoom" ADD CONSTRAINT "MuteUserOnRoom_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MuteUserOnRoom" ADD CONSTRAINT "MuteUserOnRoom_roomId_fkey" FOREIGN KEY ("roomId") REFERENCES "Room"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 72098d15..1d349bf4 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -42,6 +42,7 @@ model User { twoFactorEnabled Boolean @default(false) Message Message[] BannedRooms BanUserOnRoom[] + MutedRooms MuteUserOnRoom[] } model Room { @@ -52,6 +53,7 @@ model Room { accessLevel AccessLevel password String? BannedUsers BanUserOnRoom[] + MutedUsers MuteUserOnRoom[] } enum AccessLevel { @@ -91,6 +93,19 @@ model BanUserOnRoom { @@unique(fields: [userId, roomId], name: "userId_roomId_unique") } +model MuteUserOnRoom { + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int + + room Room @relation(fields: [roomId], references: [id], onDelete: Cascade) + roomId Int + + createdAt DateTime @default(now()) + expiresAt DateTime? + + @@unique(fields: [userId, roomId], name: "userId_roomId_unique") +} + model Message { id Int @id @default(autoincrement()) content String diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 32b812ba..fcebba10 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -10,6 +10,7 @@ import { FriendRequestModule } from './friend-request/friend-request.module'; import { HistoryModule } from './history/history.module'; import { PrismaModule } from './prisma/prisma.module'; import { BanModule } from './room/ban/ban.module'; +import { MuteModule } from './room/mute/mute.module'; import { RoomModule } from './room/room.module'; import { UserModule } from './user/user.module'; @@ -19,6 +20,7 @@ import { UserModule } from './user/user.module'; PrismaModule, AuthModule, BanModule, + MuteModule, RoomModule, EventsModule, ChatModule, diff --git a/backend/src/chat/chat.gateway.ts b/backend/src/chat/chat.gateway.ts index efad6063..c69d61a0 100644 --- a/backend/src/chat/chat.gateway.ts +++ b/backend/src/chat/chat.gateway.ts @@ -10,6 +10,7 @@ import { import { Server, Socket } from 'socket.io'; import { RoomLeftEvent } from 'src/common/events/room-left.event'; import { ChatService } from './chat.service'; +import { MuteService } from 'src/room/mute/mute.service'; import { CreateMessageDto } from './dto/create-message.dto'; import { MessageEntity } from './entities/message.entity'; @@ -21,7 +22,10 @@ import { MessageEntity } from './entities/message.entity'; cookie: true, }) export class ChatGateway { - constructor(private readonly chatService: ChatService) {} + constructor( + private readonly chatService: ChatService, + private readonly muteService: MuteService, + ) {} @WebSocketServer() server: Server; @@ -46,6 +50,11 @@ export class ChatGateway { return; } + const MutedUsers = await this.muteService.findAll(data.roomId); + if (MutedUsers.some((user) => user.id === data.userId)) { + return; + } + // Save message to the database await this.chatService.createMessage(data); diff --git a/backend/src/chat/chat.module.ts b/backend/src/chat/chat.module.ts index 40b20ee7..ee4a2bca 100644 --- a/backend/src/chat/chat.module.ts +++ b/backend/src/chat/chat.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { AuthModule } from 'src/auth/auth.module'; +import { MuteService } from 'src/room/mute/mute.service'; import { PrismaModule } from 'src/prisma/prisma.module'; import { UserService } from '../user/user.service'; import { ChatController } from './chat.controller'; @@ -8,7 +9,7 @@ import { ChatService } from './chat.service'; @Module({ controllers: [ChatController], - providers: [ChatGateway, ChatService, UserService], + providers: [ChatGateway, ChatService, MuteService, UserService], imports: [PrismaModule, AuthModule], }) export class ChatModule {} diff --git a/backend/src/room/mute/dto/create-mute.dto.ts b/backend/src/room/mute/dto/create-mute.dto.ts new file mode 100644 index 00000000..673ee2bd --- /dev/null +++ b/backend/src/room/mute/dto/create-mute.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNumber, IsOptional } from 'class-validator'; + +export class CreateMuteDto { + @IsNumber() + @IsOptional() + @ApiProperty({ required: false }) + duration: number; +} diff --git a/backend/src/room/mute/dto/update-mute.dto.ts b/backend/src/room/mute/dto/update-mute.dto.ts new file mode 100644 index 00000000..f6f543bd --- /dev/null +++ b/backend/src/room/mute/dto/update-mute.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateMuteDto } from './create-mute.dto'; + +export class UpdateMuteDto extends PartialType(CreateMuteDto) {} diff --git a/backend/src/room/mute/entities/mute.entity.ts b/backend/src/room/mute/entities/mute.entity.ts new file mode 100644 index 00000000..0f55be51 --- /dev/null +++ b/backend/src/room/mute/entities/mute.entity.ts @@ -0,0 +1 @@ +export class Mute {} diff --git a/backend/src/room/mute/mute.controller.spec.ts b/backend/src/room/mute/mute.controller.spec.ts new file mode 100644 index 00000000..f32f5c6f --- /dev/null +++ b/backend/src/room/mute/mute.controller.spec.ts @@ -0,0 +1,21 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { MuteController } from './mute.controller'; +import { MuteService } from './mute.service'; + +describe('MuteController', () => { + let controller: MuteController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [MuteController], + providers: [MuteService, PrismaService], + }).compile(); + + controller = module.get(MuteController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/room/mute/mute.controller.ts b/backend/src/room/mute/mute.controller.ts new file mode 100644 index 00000000..466b0290 --- /dev/null +++ b/backend/src/room/mute/mute.controller.ts @@ -0,0 +1,48 @@ +import { + Controller, + Get, + Put, + Body, + Delete, + Param, + ParseIntPipe, + UseGuards, +} from '@nestjs/common'; +import { CreateMuteDto } from './dto/create-mute.dto'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { JwtAuthGuard } from 'src/auth/jwt-auth.guard'; +import { AdminGuard } from '../guards/admin.guard'; +import { MuteService } from './mute.service'; + +@ApiTags('mute') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard) +@Controller('room/:roomId/mutes') +export class MuteController { + constructor(private readonly muteService: MuteService) {} + + @Put(':userId') + @UseGuards(AdminGuard) + create( + @Param('roomId', ParseIntPipe) roomId: number, + @Param('userId', ParseIntPipe) userId: number, + @Body() createMuteDto: CreateMuteDto, + ) { + return this.muteService.create(roomId, userId, createMuteDto); + } + + @Get() + @UseGuards(AdminGuard) + findAll(@Param('roomId', ParseIntPipe) roomId: number) { + return this.muteService.findAll(roomId); + } + + @Delete(':userId') + @UseGuards(AdminGuard) + remove( + @Param('roomId', ParseIntPipe) roomId: number, + @Param('userId', ParseIntPipe) userId: number, + ) { + return this.muteService.remove(roomId, userId); + } +} diff --git a/backend/src/room/mute/mute.module.ts b/backend/src/room/mute/mute.module.ts new file mode 100644 index 00000000..83ee5719 --- /dev/null +++ b/backend/src/room/mute/mute.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { PrismaModule } from 'src/prisma/prisma.module'; +import { MuteService } from './mute.service'; +import { MuteController } from './mute.controller'; + +@Module({ + controllers: [MuteController], + providers: [MuteService], + imports: [PrismaModule], +}) +export class MuteModule {} diff --git a/backend/src/room/mute/mute.service.spec.ts b/backend/src/room/mute/mute.service.spec.ts new file mode 100644 index 00000000..b4cc550f --- /dev/null +++ b/backend/src/room/mute/mute.service.spec.ts @@ -0,0 +1,19 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { MuteService } from './mute.service'; + +describe('MuteService', () => { + let service: MuteService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [MuteService, PrismaService], + }).compile(); + + service = module.get(MuteService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/room/mute/mute.service.ts b/backend/src/room/mute/mute.service.ts new file mode 100644 index 00000000..5dc250b0 --- /dev/null +++ b/backend/src/room/mute/mute.service.ts @@ -0,0 +1,136 @@ +import { + BadRequestException, + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { CreateMuteDto } from './dto/create-mute.dto'; +import { PrismaService } from 'src/prisma/prisma.service'; + +@Injectable() +export class MuteService { + constructor(private readonly prisma: PrismaService) {} + + async isExpired(roomId: number, userId: number) { + const now = new Date(); + const mute = await this.prisma.muteUserOnRoom.findUnique({ + where: { + userId_roomId_unique: { + userId, + roomId, + }, + }, + }); + if (!mute) { + return false; + } else if (mute.expiresAt === null) { + return false; + } else if (mute.expiresAt <= now) { + return true; + } else { + return false; + } + } + + async create(roomId: number, userId: number, createMuteDto: CreateMuteDto) { + await this.prisma.$transaction(async (prisma) => { + const room = await prisma.room.findUnique({ + where: { + id: roomId, + }, + }); + if (room.accessLevel === 'DIRECT') { + throw new BadRequestException('Cannot mute user in DIRECT room'); + } + const user = await prisma.userOnRoom.findUnique({ + where: { + userId_roomId_unique: { + userId, + roomId, + }, + }, + }); + if (!user) { + throw new NotFoundException('User does not exist in the room'); + } + if (user.role === 'OWNER') { + throw new ForbiddenException('Cannot mute owner'); + } + if (await this.isExpired(roomId, userId)) { + await this.remove(roomId, userId); + } + let expiresAt; + if (!createMuteDto.duration) { + expiresAt = null; + } else { + expiresAt = new Date(); + expiresAt.setSeconds(expiresAt.getSeconds() + createMuteDto.duration); + } + await prisma.muteUserOnRoom.create({ + data: { + userId, + roomId, + expiresAt, + }, + }); + }); + return { + message: 'Mute user successfully', + }; + } + + async findAll(roomId: number) { + const now = new Date(); + const tmp = await this.prisma.muteUserOnRoom.findMany({ + where: { + roomId, + OR: [ + { + expiresAt: { + gt: now, + }, + }, + { + expiresAt: null, + }, + ], + }, + include: { + user: { + select: { + id: true, + name: true, + avatarURL: true, + }, + }, + }, + }); + const users = tmp.map((item) => item.user); + return users; + } + + async remove(roomId: number, userId: number) { + await this.prisma.$transaction(async (prisma) => { + const user = await prisma.muteUserOnRoom.findUnique({ + where: { + userId_roomId_unique: { + userId, + roomId, + }, + }, + }); + if (!user) { + throw new NotFoundException('User not found in the Mute list'); + } + await prisma.muteUserOnRoom.delete({ + where: { + userId_roomId_unique: { + userId, + roomId, + }, + }, + }); + return { message: 'Unmute user successfully' }; + }); + } +} diff --git a/backend/test/chat-gateway.e2e-spec.ts b/backend/test/chat-gateway.e2e-spec.ts index 4fc9e6a7..62ceadc7 100644 --- a/backend/test/chat-gateway.e2e-spec.ts +++ b/backend/test/chat-gateway.e2e-spec.ts @@ -23,7 +23,14 @@ describe('ChatGateway and ChatController (e2e)', () => { let ws4: Socket; // Client socket 4 let ws5: Socket; // Client socket 5 let ws6: Socket; // Client socket 6 - let user1, user2, blockedUser1, blockedUser2, kickedUser1, bannedUser1; + let ws7: Socket; // Client socket 7 + let user1, + user2, + blockedUser1, + blockedUser2, + kickedUser1, + bannedUser1, + mutedUser1; beforeAll(async () => { //app = await initializeApp(); @@ -60,12 +67,18 @@ describe('ChatGateway and ChatController (e2e)', () => { email: 'banned@test.com', password: 'test-password', }; + const dto7 = { + name: 'muted-user1', + email: 'muted1@test.com', + password: 'test-password', + }; user1 = await app.createAndLoginUser(dto1); user2 = await app.createAndLoginUser(dto2); blockedUser1 = await app.createAndLoginUser(dto3); blockedUser2 = await app.createAndLoginUser(dto4); kickedUser1 = await app.createAndLoginUser(dto5); bannedUser1 = await app.createAndLoginUser(dto6); + mutedUser1 = await app.createAndLoginUser(dto7); await app .blockUser(user1.id, blockedUser1.id, user1.accessToken) .expect(200); @@ -81,6 +94,7 @@ describe('ChatGateway and ChatController (e2e)', () => { await app.deleteUser(blockedUser2.id, blockedUser2.accessToken).expect(204); await app.deleteUser(kickedUser1.id, kickedUser1.accessToken).expect(204); await app.deleteUser(bannedUser1.id, bannedUser1.accessToken).expect(204); + await app.deleteUser(mutedUser1.id, mutedUser1.accessToken).expect(204); await app.close(); ws1.close(); ws2.close(); @@ -88,6 +102,7 @@ describe('ChatGateway and ChatController (e2e)', () => { ws4.close(); ws5.close(); ws6.close(); + ws7.close(); }); const connect = (ws: Socket) => { @@ -178,12 +193,77 @@ describe('ChatGateway and ChatController (e2e)', () => { await app.enterRoom(room.id, bannedUser1.accessToken).expect(201); }); - // Setup promises to recv messages - let ctx1, ctx2: Promise; - it('Setup promises to recv messages', () => { - ctx1 = new Promise((resolve) => { - ws2.on('message', (data) => { - const expected: MessageEntity = { + describe('Typical scenario', () => { + // Setup promises to recv messages + let ctx1, ctx2: Promise; + it('Setup promises to recv messages', () => { + ctx1 = new Promise((resolve) => { + ws2.on('message', (data) => { + const expected: MessageEntity = { + user: { + id: user1.id, + name: user1.name, + avatarURL: user1.avatarURL, + }, + roomId: room.id, + content: 'hello', + }; + expect(data).toEqual(expected); + const ack = { + userId: user2.id, + roomId: room.id, + content: 'ACK: ' + data.content, + }; + ws2.emit('message', ack); + ws2.off('message'); + resolve(); + }); + }); + ctx2 = new Promise((resolve) => { + ws1.on('message', (data) => { + if (data.user.id === user1.id) return; + const expected: MessageEntity = { + user: { + id: user2.id, + name: user2.name, + avatarURL: user2.avatarURL, + }, + roomId: room.id, + content: 'ACK: hello', + }; + expect(data).toEqual(expected); + ws1.off('message'); + resolve(); + }); + }); + }); + + // Send messages + it('user1 sends messages', async () => { + const helloMessage = { + userId: user1.id, + roomId: room.id, + content: 'hello', + }; + ws1.emit('message', helloMessage); + }); + + it('user2 receives messages and send ACK', async () => { + await ctx1; + }); + + it('user1 receives ACK', async () => { + await ctx2; + }); + + it('user1 should get all messages in the room', async () => { + const res = await app + .getMessagesInRoom(room.id, user1.accessToken) + .expect(200); + const messages = res.body; + expect(messages).toHaveLength(2); + expect(messages).toEqual([ + { user: { id: user1.id, name: user1.name, @@ -191,22 +271,95 @@ describe('ChatGateway and ChatController (e2e)', () => { }, roomId: room.id, content: 'hello', - }; - expect(data).toEqual(expected); - const ack = { - userId: user2.id, + createdAt: expect.any(String), + }, + { + user: { + id: user2.id, + name: user2.name, + avatarURL: user2.avatarURL, + }, roomId: room.id, - content: 'ACK: ' + data.content, - }; - ws2.emit('message', ack); + content: 'ACK: hello', + createdAt: expect.any(String), + }, + ]); + }); + }); + describe('Block scenario', () => { + it('blockedUser1 sends message', () => { + const helloMessage = { + userId: blockedUser1.id, + roomId: room.id, + content: 'hello', + }; + ws3.emit('message', helloMessage); + }); + + it('user1 and user2 should not receive message from blockedUser1', (done) => { + const mockMessage = jest.fn(); + const mockMessage2 = jest.fn(); + ws1.on('message', mockMessage); + ws2.on('message', mockMessage2); + setTimeout(() => { + expect(mockMessage).not.toBeCalled(); + expect(mockMessage2).not.toBeCalled(); + ws1.off('message'); ws2.off('message'); - resolve(); - }); + done(); + }, 3000); + }); + + it('user1 and user2 block blockedUser2', async () => { + await app + .blockUser(user1.id, blockedUser2.id, user1.accessToken) + .expect(200); + await app + .blockUser(user2.id, blockedUser2.id, user2.accessToken) + .expect(200); + }); + + it('blockedUser2 sends message', () => { + const helloMessage = { + userId: blockedUser2.id, + roomId: room.id, + content: 'hello', + }; + ws4.emit('message', helloMessage); + }); + + it('user1 and user2 should not receive message from blockedUser2', (done) => { + const mockMessage = jest.fn(); + const mockMessage2 = jest.fn(); + ws1.on('message', mockMessage); + ws2.on('message', mockMessage2); + setTimeout(() => { + expect(mockMessage).not.toBeCalled(); + expect(mockMessage2).not.toBeCalled(); + ws1.off('message'); + ws2.off('message'); + done(); + }, 3000); }); - ctx2 = new Promise((resolve) => { - ws1.on('message', (data) => { - if (data.user.id === user1.id) return; - const expected: MessageEntity = { + + it('user1 should get all messages except from blockedUser1 and blockedUser2 in the room', async () => { + const res = await app + .getMessagesInRoom(room.id, user1.accessToken) + .expect(200); + const messages = res.body; + expect(messages).toHaveLength(2); + expect(messages).toEqual([ + { + user: { + id: user1.id, + name: user1.name, + avatarURL: user1.avatarURL, + }, + roomId: room.id, + content: 'hello', + createdAt: expect.any(String), + }, + { user: { id: user2.id, name: user2.name, @@ -214,158 +367,228 @@ describe('ChatGateway and ChatController (e2e)', () => { }, roomId: room.id, content: 'ACK: hello', - }; - expect(data).toEqual(expected); - ws1.off('message'); - resolve(); - }); + createdAt: expect.any(String), + }, + ]); }); }); - // Send messages - it('user1 sends messages', async () => { - const helloMessage = { - userId: user1.id, - roomId: room.id, - content: 'hello', - }; - ws1.emit('message', helloMessage); - }); - - it('user2 receives messages and send ACK', async () => { - await ctx1; - }); + describe('Unblock scenario', () => { + it('user1 unblocks blockedUser1', async () => { + await app + .unblockUser(user1.id, blockedUser1.id, user1.accessToken) + .expect(200); + }); - it('user1 receives ACK', async () => { - await ctx2; - }); + let ctx3, ctx4: Promise; + it('setup promises to recv messages from blockedUser1 after unblocking', async () => { + ctx3 = new Promise((resolve) => { + ws1.on('message', (data) => { + const expected: MessageEntity = { + user: { + id: blockedUser1.id, + name: blockedUser1.name, + avatarURL: blockedUser1.avatarURL, + }, + roomId: room.id, + content: 'hello', + }; + expect(data).toEqual(expected); + const ack = { + userId: user1.id, + roomId: room.id, + content: 'ACK: ' + data.content, + }; + ws1.emit('message', ack); + ws1.off('message'); + resolve(); + }); + }); + ctx4 = new Promise((resolve) => { + ws3.on('message', (data) => { + if (data.user.id === blockedUser1.id) return; + const expected: MessageEntity = { + user: { + id: user1.id, + name: user1.name, + avatarURL: user1.avatarURL, + }, + roomId: room.id, + content: 'ACK: hello', + }; + expect(data).toEqual(expected); + ws3.off('message'); + resolve(); + }); + }); + }); - it('user1 should get all messages in the room', async () => { - const res = await app - .getMessagesInRoom(room.id, user1.accessToken) - .expect(200); - const messages = res.body; - expect(messages).toHaveLength(2); - expect(messages).toEqual([ - { - user: { - id: user1.id, - name: user1.name, - avatarURL: user1.avatarURL, - }, + it('blockedUser1 sends message after unblocking', () => { + const helloMessage = { + userId: blockedUser1.id, roomId: room.id, content: 'hello', - createdAt: expect.any(String), - }, - { - user: { - id: user2.id, - name: user2.name, - avatarURL: user2.avatarURL, + }; + ws3.emit('message', helloMessage); + }); + + it('user1 receives messages and send ACK', async () => { + await ctx3; + }); + + it('blockedUser1 receives ACK', async () => { + await ctx4; + }); + + it('user1 should get all messages except from blockedUser2 in the room', async () => { + const res = await app + .getMessagesInRoom(room.id, user1.accessToken) + .expect(200); + const messages = res.body; + expect(messages).toHaveLength(5); + expect(messages).toEqual([ + { + user: { + id: user1.id, + name: user1.name, + avatarURL: user1.avatarURL, + }, + roomId: room.id, + content: 'hello', + createdAt: expect.any(String), }, - roomId: room.id, - content: 'ACK: hello', - createdAt: expect.any(String), - }, - ]); - }); + { + user: { + id: user2.id, + name: user2.name, + avatarURL: user2.avatarURL, + }, + roomId: room.id, + content: 'ACK: hello', + createdAt: expect.any(String), + }, + { + user: { + id: blockedUser1.id, + name: blockedUser1.name, + avatarURL: blockedUser1.avatarURL, + }, + roomId: room.id, + content: 'hello', + createdAt: expect.any(String), + }, + { + user: { + id: blockedUser1.id, + name: blockedUser1.name, + avatarURL: blockedUser1.avatarURL, + }, + roomId: room.id, + content: 'hello', + createdAt: expect.any(String), + }, + { + user: { + id: user1.id, + name: user1.name, + avatarURL: user1.avatarURL, + }, + roomId: room.id, + content: 'ACK: hello', + createdAt: expect.any(String), + }, + ]); + }); - it('blockedUser1 sends message', () => { - const helloMessage = { - userId: blockedUser1.id, - roomId: room.id, - content: 'hello', - }; - ws3.emit('message', helloMessage); + it('user1 unblocks blockedUser2', async () => { + await app + .unblockUser(user1.id, blockedUser2.id, user1.accessToken) + .expect(200); + }); }); - it('user1 and user2 should not receive message from blockedUser1', (done) => { - const mockMessage = jest.fn(); - const mockMessage2 = jest.fn(); - ws1.on('message', mockMessage); - ws2.on('message', mockMessage2); - setTimeout(() => { - expect(mockMessage).not.toBeCalled(); - expect(mockMessage2).not.toBeCalled(); - ws1.off('message'); - ws2.off('message'); - done(); - }, 3000); - }); + describe('Kick scenario', () => { + let ctx5: Promise; + it('setup promises to recv leave event with user id', async () => { + const expectedEvent = { + userId: kickedUser1.id, + roomId: room.id, + }; + const promises = [ws1, ws2, ws3, ws4, ws5, ws6].map( + (ws) => + new Promise((resolve) => { + ws.on('leave', (data) => { + expect(data).toEqual(expectedEvent); + ws.off('leave'); + resolve(); + }); + }), + ); - it('user1 and user2 block blockedUser2', async () => { - await app - .blockUser(user1.id, blockedUser2.id, user1.accessToken) - .expect(200); - await app - .blockUser(user2.id, blockedUser2.id, user2.accessToken) - .expect(200); - }); + ctx5 = Promise.all(promises); + }); - it('blockedUser2 sends message', () => { - const helloMessage = { - userId: blockedUser2.id, - roomId: room.id, - content: 'hello', - }; - ws4.emit('message', helloMessage); - }); + it('user1 kicks kickedUser1', async () => { + await app + .kickFromRoom(room.id, kickedUser1.id, user1.accessToken) + .expect(204); + }); - it('user1 and user2 should not receive message from blockedUser2', (done) => { - const mockMessage = jest.fn(); - const mockMessage2 = jest.fn(); - ws1.on('message', mockMessage); - ws2.on('message', mockMessage2); - setTimeout(() => { - expect(mockMessage).not.toBeCalled(); - expect(mockMessage2).not.toBeCalled(); - ws1.off('message'); - ws2.off('message'); - done(); - }, 3000); - }); + it('all users (except kickedUser1) should receive leave event with kickedUser1 id', async () => { + await ctx5; + }); - it('user1 should get all messages except from blockedUser1 and blockedUser2 in the room', async () => { - const res = await app - .getMessagesInRoom(room.id, user1.accessToken) - .expect(200); - const messages = res.body; - expect(messages).toHaveLength(2); - expect(messages).toEqual([ - { - user: { - id: user1.id, - name: user1.name, - avatarURL: user1.avatarURL, - }, + it('kickedUser1 sends message', () => { + const helloMessage = { + userId: kickedUser1.id, roomId: room.id, content: 'hello', - createdAt: expect.any(String), - }, - { - user: { - id: user2.id, - name: user2.name, - avatarURL: user2.avatarURL, - }, - roomId: room.id, - content: 'ACK: hello', - createdAt: expect.any(String), - }, - ]); - }); + }; + ws5.emit('message', helloMessage); + }); - it('user1 unblocks blockedUser1', async () => { - await app - .unblockUser(user1.id, blockedUser1.id, user1.accessToken) - .expect(200); - }); + it('user1 and user2 should not receive message from kickedUser1', (done) => { + const mockMessage = jest.fn(); + const mockMessage2 = jest.fn(); + ws1.on('message', mockMessage); + ws2.on('message', mockMessage2); + setTimeout(() => { + expect(mockMessage).not.toBeCalled(); + expect(mockMessage2).not.toBeCalled(); + ws1.off('message'); + ws2.off('message'); + done(); + }, 3000); + }); - let ctx3, ctx4: Promise; - it('setup promises to recv messages from blockedUser1 after unblocking', async () => { - ctx3 = new Promise((resolve) => { - ws1.on('message', (data) => { - const expected: MessageEntity = { + it('user1 should get all messages except from kickedUser1 in the room', async () => { + const res = await app + .getMessagesInRoom(room.id, user1.accessToken) + .expect(200); + const messages = res.body; + expect(messages).toHaveLength(6); + expect(messages).toEqual([ + { + user: { + id: user1.id, + name: user1.name, + avatarURL: user1.avatarURL, + }, + roomId: room.id, + content: 'hello', + createdAt: expect.any(String), + }, + { + user: { + id: user2.id, + name: user2.name, + avatarURL: user2.avatarURL, + }, + roomId: room.id, + content: 'ACK: hello', + createdAt: expect.any(String), + }, + { user: { id: blockedUser1.id, name: blockedUser1.name, @@ -373,22 +596,29 @@ describe('ChatGateway and ChatController (e2e)', () => { }, roomId: room.id, content: 'hello', - }; - expect(data).toEqual(expected); - const ack = { - userId: user1.id, + createdAt: expect.any(String), + }, + { + user: { + id: blockedUser2.id, + name: blockedUser2.name, + avatarURL: blockedUser2.avatarURL, + }, roomId: room.id, - content: 'ACK: ' + data.content, - }; - ws1.emit('message', ack); - ws1.off('message'); - resolve(); - }); - }); - ctx4 = new Promise((resolve) => { - ws3.on('message', (data) => { - if (data.user.id === blockedUser1.id) return; - const expected: MessageEntity = { + content: 'hello', + createdAt: expect.any(String), + }, + { + user: { + id: blockedUser1.id, + name: blockedUser1.name, + avatarURL: blockedUser1.avatarURL, + }, + roomId: room.id, + content: 'hello', + createdAt: expect.any(String), + }, + { user: { id: user1.id, name: user1.name, @@ -396,363 +626,331 @@ describe('ChatGateway and ChatController (e2e)', () => { }, roomId: room.id, content: 'ACK: hello', - }; - expect(data).toEqual(expected); - ws3.off('message'); - resolve(); - }); + createdAt: expect.any(String), + }, + ]); }); - }); - it('blockedUser1 sends message after unblocking', () => { - const helloMessage = { - userId: blockedUser1.id, - roomId: room.id, - content: 'hello', - }; - ws3.emit('message', helloMessage); - }); + it('user1 sends message', () => { + const helloMessage = { + userId: user1.id, + roomId: room.id, + content: 'hello', + }; + ws1.emit('message', helloMessage); + }); - it('user1 receives messages and send ACK', async () => { - await ctx3; + it('kickedUser1 should not receive message from user1', (done) => { + const mockMessage = jest.fn(); + ws5.on('message', mockMessage); + setTimeout(() => { + expect(mockMessage).not.toBeCalled(); + ws5.off('message'); + done(); + }, 3000); + }); }); - it('blockedUser1 receives ACK', async () => { - await ctx4; - }); + describe('Ban scenario', () => { + it('user1 bans bannedUser1', async () => { + await app + .banUser(room.id, bannedUser1.id, user1.accessToken) + .expect(200); + }); - it('user1 should get all messages except from blockedUser2 in the room', async () => { - const res = await app - .getMessagesInRoom(room.id, user1.accessToken) - .expect(200); - const messages = res.body; - expect(messages).toHaveLength(5); - expect(messages).toEqual([ - { - user: { - id: user1.id, - name: user1.name, - avatarURL: user1.avatarURL, - }, + it('bannedUser1 sends message', () => { + const helloMessage = { + userId: bannedUser1.id, roomId: room.id, content: 'hello', - createdAt: expect.any(String), - }, - { - user: { - id: user2.id, - name: user2.name, - avatarURL: user2.avatarURL, + }; + ws6.emit('message', helloMessage); + }); + + it('user1 and user2 should not receive message from bannedUser1', (done) => { + const mockMessage = jest.fn(); + const mockMessage2 = jest.fn(); + ws1.on('message', mockMessage); + ws2.on('message', mockMessage2); + setTimeout(() => { + expect(mockMessage).not.toBeCalled(); + expect(mockMessage2).not.toBeCalled(); + ws1.off('message'); + ws2.off('message'); + done(); + }, 3000); + }); + + it('user1 should get all messages except from bannedUser1 in the room', async () => { + const res = await app + .getMessagesInRoom(room.id, user1.accessToken) + .expect(200); + const messages = res.body; + expect(messages).toHaveLength(7); + expect(messages).toEqual([ + { + user: { + id: user1.id, + name: user1.name, + avatarURL: user1.avatarURL, + }, + roomId: room.id, + content: 'hello', + createdAt: expect.any(String), }, - roomId: room.id, - content: 'ACK: hello', - createdAt: expect.any(String), - }, - { - user: { - id: blockedUser1.id, - name: blockedUser1.name, - avatarURL: blockedUser1.avatarURL, + { + user: { + id: user2.id, + name: user2.name, + avatarURL: user2.avatarURL, + }, + roomId: room.id, + content: 'ACK: hello', + createdAt: expect.any(String), }, - roomId: room.id, - content: 'hello', - createdAt: expect.any(String), - }, - { - user: { - id: blockedUser1.id, - name: blockedUser1.name, - avatarURL: blockedUser1.avatarURL, + { + user: { + id: blockedUser1.id, + name: blockedUser1.name, + avatarURL: blockedUser1.avatarURL, + }, + roomId: room.id, + content: 'hello', + createdAt: expect.any(String), }, - roomId: room.id, - content: 'hello', - createdAt: expect.any(String), - }, - { - user: { - id: user1.id, - name: user1.name, - avatarURL: user1.avatarURL, + { + user: { + id: blockedUser2.id, + name: blockedUser2.name, + avatarURL: blockedUser2.avatarURL, + }, + roomId: room.id, + content: 'hello', + createdAt: expect.any(String), + }, + { + user: { + id: blockedUser1.id, + name: blockedUser1.name, + avatarURL: blockedUser1.avatarURL, + }, + roomId: room.id, + content: 'hello', + createdAt: expect.any(String), + }, + { + user: { + id: user1.id, + name: user1.name, + avatarURL: user1.avatarURL, + }, + roomId: room.id, + content: 'ACK: hello', + createdAt: expect.any(String), + }, + { + user: { + id: user1.id, + name: user1.name, + avatarURL: user1.avatarURL, + }, + roomId: room.id, + content: 'hello', + createdAt: expect.any(String), }, + ]); + }); + + it('user1 sends message', () => { + const helloMessage = { + userId: user1.id, roomId: room.id, - content: 'ACK: hello', - createdAt: expect.any(String), - }, - ]); - }); + content: 'hello', + }; + ws1.emit('message', helloMessage); + }); - it('user1 unblocks blockedUser2', async () => { - await app - .unblockUser(user1.id, blockedUser2.id, user1.accessToken) - .expect(200); + it('bannedUser1 should not receive message from user1', (done) => { + const mockMessage = jest.fn(); + ws6.on('message', mockMessage); + setTimeout(() => { + expect(mockMessage).not.toBeCalled(); + ws6.off('message'); + done(); + }, 3000); + }); }); + }); - let ctx5: Promise; - it('setup promises to recv leave event with user id', async () => { - const expectedEvent = { - userId: kickedUser1.id, - roomId: room.id, - }; - const promises = [ws1, ws2, ws3, ws4, ws5, ws6].map( - (ws) => - new Promise((resolve) => { - ws.on('leave', (data) => { - expect(data).toEqual(expectedEvent); - ws.off('leave'); - resolve(); - }); - }), - ); - - ctx5 = Promise.all(promises); + describe('Mute scenario', () => { + afterAll(async () => { + await app.deleteRoom(room.id, user1.accessToken).expect(204); + }); + it('Connect to chat server as mutedUser1', async () => { + ws7 = io('ws://localhost:3000/chat', { + extraHeaders: { cookie: 'token=' + mutedUser1.accessToken }, + }); + expect(ws7).toBeDefined(); + await connect(ws7); }); - it('user1 kicks kickedUser1', async () => { - await app - .kickFromRoom(room.id, kickedUser1.id, user1.accessToken) - .expect(204); + let room; + it('Create and enter a room', async () => { + const res = await app + .createRoom(constants.room.publicRoom, user1.accessToken) + .expect(201); + room = res.body; + expect(room.id).toBeDefined(); + await app.enterRoom(room.id, user2.accessToken).expect(201); + await app.enterRoom(room.id, blockedUser1.accessToken).expect(201); + await app.enterRoom(room.id, blockedUser2.accessToken).expect(201); + await app.enterRoom(room.id, kickedUser1.accessToken).expect(201); + await app.enterRoom(room.id, bannedUser1.accessToken).expect(201); + await app.enterRoom(room.id, mutedUser1.accessToken).expect(201); }); - it('all users (except kickedUser1) should receive leave event with kickedUser1 id', async () => { - await ctx5; + it('user1 mutes mutedUser1', async () => { + await app + .muteUser(room.id, mutedUser1.id, user1.accessToken, 3) + .expect(200); }); - it('kickedUser1 sends message', () => { + it('mutedUser1 sends message', () => { const helloMessage = { - userId: kickedUser1.id, + userId: mutedUser1.id, roomId: room.id, content: 'hello', }; - ws5.emit('message', helloMessage); + ws7.emit('message', helloMessage); }); - it('user1 and user2 should not receive message from kickedUser1', (done) => { + it('all users should not receive message from mutedUser1', (done) => { const mockMessage = jest.fn(); const mockMessage2 = jest.fn(); + const mockMessage3 = jest.fn(); + const mockMessage4 = jest.fn(); + const mockMessage5 = jest.fn(); + const mockMessage6 = jest.fn(); ws1.on('message', mockMessage); ws2.on('message', mockMessage2); + ws3.on('message', mockMessage3); + ws4.on('message', mockMessage4); + ws5.on('message', mockMessage5); + ws6.on('message', mockMessage6); setTimeout(() => { expect(mockMessage).not.toBeCalled(); expect(mockMessage2).not.toBeCalled(); + expect(mockMessage3).not.toBeCalled(); + expect(mockMessage4).not.toBeCalled(); + expect(mockMessage5).not.toBeCalled(); + expect(mockMessage6).not.toBeCalled(); ws1.off('message'); ws2.off('message'); + ws3.off('message'); + ws4.off('message'); + ws5.off('message'); + ws6.off('message'); done(); }, 3000); }); - it('user1 should get all messages except from kickedUser1 in the room', async () => { - const res = await app - .getMessagesInRoom(room.id, user1.accessToken) - .expect(200); - const messages = res.body; - expect(messages).toHaveLength(6); - expect(messages).toEqual([ - { - user: { - id: user1.id, - name: user1.name, - avatarURL: user1.avatarURL, - }, - roomId: room.id, - content: 'hello', - createdAt: expect.any(String), - }, - { - user: { - id: user2.id, - name: user2.name, - avatarURL: user2.avatarURL, - }, - roomId: room.id, - content: 'ACK: hello', - createdAt: expect.any(String), + let ctx6: Promise; + it('setup promises to recv messages from mutedUser1 after the duration', async () => { + await new Promise((r) => setTimeout(r, 4000)); + const expected: MessageEntity = { + user: { + id: mutedUser1.id, + name: mutedUser1.name, + avatarURL: mutedUser1.avatarURL, }, - { - user: { - id: blockedUser1.id, - name: blockedUser1.name, - avatarURL: blockedUser1.avatarURL, - }, - roomId: room.id, - content: 'hello', - createdAt: expect.any(String), - }, - { - user: { - id: blockedUser2.id, - name: blockedUser2.name, - avatarURL: blockedUser2.avatarURL, - }, - roomId: room.id, - content: 'hello', - createdAt: expect.any(String), - }, - { - user: { - id: blockedUser1.id, - name: blockedUser1.name, - avatarURL: blockedUser1.avatarURL, - }, - roomId: room.id, - content: 'hello', - createdAt: expect.any(String), - }, - { - user: { - id: user1.id, - name: user1.name, - avatarURL: user1.avatarURL, - }, - roomId: room.id, - content: 'ACK: hello', - createdAt: expect.any(String), - }, - ]); + roomId: room.id, + content: 'hello', + }; + const promises = [ws1, ws2, ws3, ws4, ws5, ws6].map( + (ws) => + new Promise((resolve) => { + ws.on('message', (data) => { + expect(data).toEqual(expected); + ws.off('message'); + resolve(); + }); + }), + ); + ctx6 = Promise.all(promises); }); - it('user1 sends message', () => { + it('mutedUser1 sends message after the duration', () => { const helloMessage = { - userId: user1.id, + userId: mutedUser1.id, roomId: room.id, content: 'hello', }; - ws1.emit('message', helloMessage); + ws7.emit('message', helloMessage); }); - it('kickedUser1 should not receive message from user1', (done) => { - const mockMessage = jest.fn(); - ws5.on('message', mockMessage); - setTimeout(() => { - expect(mockMessage).not.toBeCalled(); - ws5.off('message'); - done(); - }, 3000); + it('all users should receive message from mutedUser1 after the duration', async () => { + await ctx6; }); - it('user1 bans bannedUser1', async () => { - await app.banUser(room.id, bannedUser1.id, user1.accessToken).expect(200); + it('user1 unmutes mutedUser1', async () => { + await app + .unmuteUser(room.id, mutedUser1.id, user1.accessToken) + .expect(200); }); - it('bannedUser1 sends message', () => { + it('user1 mute mutedUser1 again', async () => { + await app + .muteUser(room.id, mutedUser1.id, user1.accessToken, 10) + .expect(200); + }); + + it('mutedUser1 sends message', () => { const helloMessage = { - userId: bannedUser1.id, + userId: mutedUser1.id, roomId: room.id, content: 'hello', }; - ws6.emit('message', helloMessage); + ws7.emit('message', helloMessage); }); - it('user1 and user2 should not receive message from bannedUser1', (done) => { + it('all users should not receive message from mutedUser1', (done) => { const mockMessage = jest.fn(); const mockMessage2 = jest.fn(); + const mockMessage3 = jest.fn(); + const mockMessage4 = jest.fn(); + const mockMessage5 = jest.fn(); + const mockMessage6 = jest.fn(); ws1.on('message', mockMessage); ws2.on('message', mockMessage2); + ws3.on('message', mockMessage3); + ws4.on('message', mockMessage4); + ws5.on('message', mockMessage5); + ws6.on('message', mockMessage6); setTimeout(() => { expect(mockMessage).not.toBeCalled(); expect(mockMessage2).not.toBeCalled(); + expect(mockMessage3).not.toBeCalled(); + expect(mockMessage4).not.toBeCalled(); + expect(mockMessage5).not.toBeCalled(); + expect(mockMessage6).not.toBeCalled(); ws1.off('message'); ws2.off('message'); + ws3.off('message'); + ws4.off('message'); + ws5.off('message'); + ws6.off('message'); done(); }, 3000); }); - it('user1 should get all messages except from bannedUser1 in the room', async () => { - const res = await app - .getMessagesInRoom(room.id, user1.accessToken) + it('user1 unmutes mutedUser1', async () => { + await app + .unmuteUser(room.id, mutedUser1.id, user1.accessToken) .expect(200); - const messages = res.body; - expect(messages).toHaveLength(7); - expect(messages).toEqual([ - { - user: { - id: user1.id, - name: user1.name, - avatarURL: user1.avatarURL, - }, - roomId: room.id, - content: 'hello', - createdAt: expect.any(String), - }, - { - user: { - id: user2.id, - name: user2.name, - avatarURL: user2.avatarURL, - }, - roomId: room.id, - content: 'ACK: hello', - createdAt: expect.any(String), - }, - { - user: { - id: blockedUser1.id, - name: blockedUser1.name, - avatarURL: blockedUser1.avatarURL, - }, - roomId: room.id, - content: 'hello', - createdAt: expect.any(String), - }, - { - user: { - id: blockedUser2.id, - name: blockedUser2.name, - avatarURL: blockedUser2.avatarURL, - }, - roomId: room.id, - content: 'hello', - createdAt: expect.any(String), - }, - { - user: { - id: blockedUser1.id, - name: blockedUser1.name, - avatarURL: blockedUser1.avatarURL, - }, - roomId: room.id, - content: 'hello', - createdAt: expect.any(String), - }, - { - user: { - id: user1.id, - name: user1.name, - avatarURL: user1.avatarURL, - }, - roomId: room.id, - content: 'ACK: hello', - createdAt: expect.any(String), - }, - { - user: { - id: user1.id, - name: user1.name, - avatarURL: user1.avatarURL, - }, - roomId: room.id, - content: 'hello', - createdAt: expect.any(String), - }, - ]); - }); - - it('user1 sends message', () => { - const helloMessage = { - userId: user1.id, - roomId: room.id, - content: 'hello', - }; - ws1.emit('message', helloMessage); }); - it('bannedUser1 should not receive message from user1', (done) => { - const mockMessage = jest.fn(); - ws6.on('message', mockMessage); - setTimeout(() => { - expect(mockMessage).not.toBeCalled(); - ws6.off('message'); - done(); - }, 3000); + it('all users should receive message from mutedUser1 after unmuting', async () => { + await ctx6; }); }); diff --git a/backend/test/room.e2e-spec.ts b/backend/test/room.e2e-spec.ts index 4bf925a2..2cd14e1e 100644 --- a/backend/test/room.e2e-spec.ts +++ b/backend/test/room.e2e-spec.ts @@ -1890,25 +1890,383 @@ describe('RoomController (e2e)', () => { it('notMember should not unban anyone', async () => {}); }); - describe('POST /room/:id/mutes/:userId (Mute user)', () => { - it('owner should mute anyone in the room', async () => {}); - it('admin should mute admin/member', async () => {}); - it('member should not mute anyone', async () => {}); - it('notMember should not mute anyone', async () => {}); + describe('PUT /room/:id/mutes/:userId (Mute user)', () => { + const duration = 3; + describe('Owner', () => { + let _publicRoom: RoomEntity; + beforeAll(async () => { + _publicRoom = await setupRoom(constants.room.publicRoom); + }); + afterAll(async () => { + await app.deleteRoom(_publicRoom.id, owner.accessToken); + }); + it('should mute anyone in the room (200 OK)', async () => { + await app + .muteUser(_publicRoom.id, admin.id, owner.accessToken, duration) + .expect(200); + await app + .muteUser(_publicRoom.id, member.id, owner.accessToken, duration) + .expect(200); + }); + }); + describe('Admin', () => { + let _publicRoom: RoomEntity; + let _admin2: UserEntityWithAccessToken; + beforeAll(async () => { + const dto = { + ...constants.user.admin, + name: 'admin2', + email: 'admin2@example.com', + }; + _admin2 = await app.createAndLoginUser(dto); + _publicRoom = await setupRoom(constants.room.publicRoom); + await app.inviteRoom(_publicRoom.id, _admin2.id, admin.accessToken); + }); + afterAll(async () => { + await app.deleteRoom(_publicRoom.id, owner.accessToken); + await app.deleteUser(_admin2.id, _admin2.accessToken).expect(204); + }); + it('should not mute owner (403 Forbidden)', async () => { + await app + .muteUser(_publicRoom.id, owner.id, admin.accessToken, duration) + .expect(403); + }); + it('should mute admin/member in the room (200 OK)', async () => { + await app + .muteUser(_publicRoom.id, _admin2.id, admin.accessToken, duration) + .expect(200); + await app + .muteUser(_publicRoom.id, member.id, admin.accessToken, duration) + .expect(200); + }); + }); + describe('Member', () => { + let _publicRoom: RoomEntity; + let _member2: UserEntityWithAccessToken; + beforeAll(async () => { + const dto = { + ...constants.user.member, + name: 'member3', + email: 'member3@example.com', + }; + _member2 = await app.createAndLoginUser(dto); + _publicRoom = await setupRoom(constants.room.publicRoom); + await app.inviteRoom(_publicRoom.id, _member2.id, admin.accessToken); + }); + afterAll(async () => { + await app.deleteRoom(_publicRoom.id, owner.accessToken); + await app.deleteUser(_member2.id, _member2.accessToken).expect(204); + }); + it('should not mute anyone (403 Forbidden)', async () => { + await app + .muteUser(_publicRoom.id, owner.id, member.accessToken, duration) + .expect(403); + await app + .muteUser(_publicRoom.id, admin.id, member.accessToken, duration) + .expect(403); + await app + .muteUser(_publicRoom.id, _member2.id, member.accessToken, duration) + .expect(403); + await app + .muteUser(_publicRoom.id, notMember.id, member.accessToken, duration) + .expect(403); + }); + }); + describe('Non-member', () => { + let _publicRoom: RoomEntity; + beforeAll(async () => { + _publicRoom = await setupRoom(constants.room.publicRoom); + }); + afterAll(async () => { + await app.deleteRoom(_publicRoom.id, owner.accessToken); + }); + it('should not mute anyone (403 Forbidden)', async () => { + await app + .muteUser(_publicRoom.id, owner.id, notMember.accessToken, duration) + .expect(403); + await app + .muteUser(_publicRoom.id, admin.id, notMember.accessToken, duration) + .expect(403); + await app + .muteUser(_publicRoom.id, member.id, notMember.accessToken, duration) + .expect(403); + await app + .muteUser( + _publicRoom.id, + notMember.id, + notMember.accessToken, + duration, + ) + .expect(403); + }); + }); - it('muted user should not be able to speak', async () => {}); - it('muted user should be able to speak again after the duration', async () => {}); + describe('Mute Scenario', () => { + let _publicRoom: RoomEntity; + beforeAll(async () => { + _publicRoom = await setupRoom(constants.room.publicRoom); + }); + afterAll(async () => { + await app.deleteRoom(_publicRoom.id, owner.accessToken); + }); + it('should mute user (200 OK)', async () => { + await app + .muteUser(_publicRoom.id, member.id, owner.accessToken, duration) + .expect(200); + }); + it('should not mute user who is already muted (409 Conflict)', async () => { + await app + .muteUser(_publicRoom.id, member.id, owner.accessToken, duration) + .expect(409); + await app + .muteUser(_publicRoom.id, member.id, admin.accessToken, duration) + .expect(409); + }); + it('should mute user after the duration (200 OK)', async () => { + await new Promise((r) => setTimeout(r, 5000)); + await app + .muteUser(_publicRoom.id, member.id, owner.accessToken, duration) + .expect(200); + }, 6000); + it('should mute user forever (200 OK)', async () => { + await app + .muteUser(_publicRoom.id, admin.id, owner.accessToken) + .expect(200); + }); + it('should not mute user who is already muted forever (409 Conflict)', async () => { + await app + .muteUser(_publicRoom.id, admin.id, owner.accessToken) + .expect(409); + }); + it('should not mute user who is not in the room (404 Not Found)', async () => { + await app + .muteUser(_publicRoom.id, notMember.id, owner.accessToken, duration) + .expect(404); + await app + .muteUser(_publicRoom.id, notMember.id, admin.accessToken, duration) + .expect(404); + }); + }); + }); - it('should not mute user who is already muted', async () => {}); - it('should not mute user who is not in the room', async () => {}); + describe('GET /room/:id/mutes (Get muted users)', () => { + let _publicRoom: RoomEntity; + let mutedUser: UserEntityWithAccessToken; + beforeAll(async () => { + mutedUser = await app.createAndLoginUser({ + name: 'MUTED USER', + email: 'muted@example.com', + password: '12345678', + }); + _publicRoom = await setupRoom(constants.room.publicRoom); + await app.inviteRoom(_publicRoom.id, mutedUser.id, owner.accessToken); + }); + afterAll(async () => { + await app.deleteRoom(_publicRoom.id, owner.accessToken).expect(204); + await app.deleteUser(mutedUser.id, mutedUser.accessToken).expect(204); + }); + describe('Owner', () => { + it('should get empty array when no muted users in the room (200 OK)', async () => { + const res = await app + .getMutedUsers(_publicRoom.id, owner.accessToken) + .expect(200); + expect(res.body).toBeInstanceOf(Array); + expect(res.body).toHaveLength(0); + }); + it('should mute user (200 OK)', async () => { + await app + .muteUser(_publicRoom.id, mutedUser.id, owner.accessToken, 1) + .expect(200); + }); + it('should get muted users (200 OK)', async () => { + const res = await app + .getMutedUsers(_publicRoom.id, owner.accessToken) + .expect(200); + expect(res.body).toBeInstanceOf(Array); + res.body.forEach(expectPublicUser); + const publicMutedUser = { + id: mutedUser.id, + name: mutedUser.name, + avatarURL: mutedUser.avatarURL, + }; + expect(res.body).toContainEqual(publicMutedUser); + }); + }); + describe('Admin', () => { + it('should get muted users (200 OK)', async () => { + await app.getMutedUsers(_publicRoom.id, admin.accessToken).expect(200); + }); + }); + describe('Member', () => { + it('should not get muted users (403 Forbidden)', async () => { + await app.getMutedUsers(_publicRoom.id, member.accessToken).expect(403); + }); + }); + describe('Non-member', () => { + it('should not get muted users (403 Forbidden)', async () => { + await app + .getMutedUsers(_publicRoom.id, notMember.accessToken) + .expect(403); + }); + }); + describe('Owner and Admin', () => { + it('should get empty array after the duration (200 OK)', (done) => { + setTimeout(async () => { + const res = await app + .getMutedUsers(_publicRoom.id, owner.accessToken) + .expect(200); + expect(res.body).toBeInstanceOf(Array); + expect(res.body).toHaveLength(0); + const res2 = await app + .getMutedUsers(_publicRoom.id, admin.accessToken) + .expect(200); + expect(res2.body).toBeInstanceOf(Array); + expect(res2.body).toHaveLength(0); + done(); + }, 2000); + }); + }); }); describe('DELETE /room/:id/mutes/:userId (Unmute user)', () => { - it('owner should unmute anyone in the room', async () => {}); - it('admin should unmute admin/member', async () => {}); - it('member should not unmute anyone', async () => {}); - it('notMember should not unmute anyone', async () => {}); - - it('unmuted user should be able to speak', async () => {}); + describe('Owner', () => { + let _publicRoom: RoomEntity; + beforeAll(async () => { + _publicRoom = await setupRoom(constants.room.publicRoom); + await app + .muteUser(_publicRoom.id, admin.id, owner.accessToken, 10) + .expect(200); + await app + .muteUser(_publicRoom.id, member.id, owner.accessToken, 10) + .expect(200); + }); + afterAll(async () => { + await app.deleteRoom(_publicRoom.id, owner.accessToken).expect(204); + }); + it('should unmute anyone in the room (200 OK)', async () => { + await app + .unmuteUser(_publicRoom.id, admin.id, owner.accessToken) + .expect(200); + await app + .unmuteUser(_publicRoom.id, member.id, owner.accessToken) + .expect(200); + }); + it('should not unmute non-member (404 Not Found)', async () => { + await app + .unmuteUser(_publicRoom.id, notMember.id, owner.accessToken) + .expect(404); + }); + }); + describe('Admin', () => { + let _publicRoom: RoomEntity; + let _admin2: UserEntityWithAccessToken; + beforeAll(async () => { + const dto = { + ...constants.user.admin, + name: 'admin2', + email: 'admin2@example.com', + password: '12345678', + }; + _admin2 = await app.createAndLoginUser(dto); + _publicRoom = await setupRoom(constants.room.publicRoom); + await app.inviteRoom(_publicRoom.id, _admin2.id, admin.accessToken); + await app + .muteUser(_publicRoom.id, _admin2.id, admin.accessToken, 10) + .expect(200); + await app + .muteUser(_publicRoom.id, member.id, admin.accessToken, 10) + .expect(200); + }); + afterAll(async () => { + await app.deleteRoom(_publicRoom.id, owner.accessToken).expect(204); + await app.deleteUser(_admin2.id, _admin2.accessToken).expect(204); + }); + it('should not unmute owner in the room (404 Not Found)', async () => { + await app + .unmuteUser(_publicRoom.id, owner.id, admin.accessToken) + .expect(404); + }); + it('should unmute admin/member (200 OK)', async () => { + await app + .unmuteUser(_publicRoom.id, _admin2.id, admin.accessToken) + .expect(200); + await app + .unmuteUser(_publicRoom.id, member.id, admin.accessToken) + .expect(200); + }); + it('should not unmute non-member (404 Not Found)', async () => { + await app + .unmuteUser(_publicRoom.id, notMember.id, admin.accessToken) + .expect(404); + }); + }); + describe('Member', () => { + let _publicRoom: RoomEntity; + let _member2: UserEntityWithAccessToken; + beforeAll(async () => { + const dto = { + ...constants.user.member, + name: 'member3', + email: 'member3@example.com', + password: '12345678', + }; + _member2 = await app.createAndLoginUser(dto); + _publicRoom = await setupRoom(constants.room.publicRoom); + await app.inviteRoom(_publicRoom.id, _member2.id, admin.accessToken); + await app + .muteUser(_publicRoom.id, admin.id, owner.accessToken, 10) + .expect(200); + await app + .muteUser(_publicRoom.id, _member2.id, owner.accessToken, 10) + .expect(200); + }); + afterAll(async () => { + await app.deleteRoom(_publicRoom.id, owner.accessToken).expect(204); + await app.deleteUser(_member2.id, _member2.accessToken).expect(204); + }); + it('should not unmute anyone (403 Forbidden)', async () => { + await app + .unmuteUser(_publicRoom.id, owner.id, member.accessToken) + .expect(403); + await app + .unmuteUser(_publicRoom.id, admin.id, member.accessToken) + .expect(403); + await app + .unmuteUser(_publicRoom.id, _member2.id, member.accessToken) + .expect(403); + await app + .unmuteUser(_publicRoom.id, notMember.id, member.accessToken) + .expect(403); + }); + }); + describe('Non-member', () => { + let _publicRoom: RoomEntity; + beforeAll(async () => { + _publicRoom = await setupRoom(constants.room.publicRoom); + await app + .muteUser(_publicRoom.id, admin.id, owner.accessToken, 10) + .expect(200); + await app + .muteUser(_publicRoom.id, member.id, owner.accessToken, 10) + .expect(200); + }); + afterAll(async () => { + await app.deleteRoom(_publicRoom.id, owner.accessToken).expect(204); + }); + it('should not unmute anyone (403 Forbidden)', async () => { + await app + .unmuteUser(_publicRoom.id, owner.id, notMember.accessToken) + .expect(403); + await app + .unmuteUser(_publicRoom.id, admin.id, notMember.accessToken) + .expect(403); + await app + .unmuteUser(_publicRoom.id, member.id, notMember.accessToken) + .expect(403); + await app + .unmuteUser(_publicRoom.id, notMember.id, notMember.accessToken) + .expect(403); + }); + }); }); }); diff --git a/backend/test/utils/app.ts b/backend/test/utils/app.ts index c7b4548e..2d6015a5 100644 --- a/backend/test/utils/app.ts +++ b/backend/test/utils/app.ts @@ -144,8 +144,8 @@ export class TestApp { muteUser = ( roomId: number, userId: number, - duration: number, accessToken: string, + duration?: number, ) => request(this.app.getHttpServer()) .put(`/room/${roomId}/mutes/${userId}`) @@ -157,6 +157,11 @@ export class TestApp { .delete(`/room/${roomId}/mutes/${userId}`) .set('Authorization', `Bearer ${accessToken}`); + getMutedUsers = (roomId: number, accessToken: string) => + request(this.app.getHttpServer()) + .get(`/room/${roomId}/mutes`) + .set('Authorization', `Bearer ${accessToken}`); + getMessagesInRoom = (roomId: number, accessToken: string) => request(this.app.getHttpServer()) .get(`/room/${roomId}/messages`)