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

[backend] handling owner leaves room and delete room notification #249

Merged
merged 10 commits into from
Feb 8, 2024
17 changes: 15 additions & 2 deletions backend/src/chat/chat.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { RoomDeletedEvent } from 'src/common/events/room-deleted.event';
import { RoomEnteredEvent } from 'src/common/events/room-entered.event';
import { RoomMuteEvent } from 'src/common/events/room-mute.event';
import { RoomUnmuteEvent } from 'src/common/events/room-unmute.event';
Expand Down Expand Up @@ -166,16 +167,28 @@ export class ChatGateway {
this.server.emit('online-status', event);
}

@OnEvent('room.delete', { async: true })
async handleDelete(event: RoomDeletedEvent) {
if (event.accessLevel === 'PUBLIC' || event.accessLevel === 'PROTECTED') {
this.server.emit('delete-room', { roomId: event.roomId });
} else {
this.server
.in(event.roomId.toString())
.emit('delete-room', { roomId: event.roomId });
}
this.chatService.deleteSocketRoom(event);
}

@OnEvent('room.enter', { async: true })
async handleEnter(event: RoomEnteredEvent) {
await this.chatService.addUserToRoom(event.roomId, event.userId);
this.chatService.addUserToRoom(event.roomId, event.userId);
this.server.in(event.roomId.toString()).emit('enter-room', event);
}

@OnEvent('room.leave', { async: true })
async handleLeave(event: RoomLeftEvent) {
this.server.in(event.roomId.toString()).emit('leave', event);
await this.chatService.removeUserFromRoom(event);
this.chatService.removeUserFromRoom(event.roomId, event.userId);
}

@OnEvent('room.update.role', { async: true })
Expand Down
27 changes: 16 additions & 11 deletions backend/src/chat/chat.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Socket } from 'socket.io';
import { AuthService } from 'src/auth/auth.service';
import { BlockEvent } from 'src/common/events/block.event';
import { RoomCreatedEvent } from 'src/common/events/room-created.event';
import { RoomLeftEvent } from 'src/common/events/room-left.event';
import { RoomDeletedEvent } from 'src/common/events/room-deleted.event';
import { UnblockEvent } from 'src/common/events/unblock.event';
import { PrismaService } from 'src/prisma/prisma.service';
import { UserService } from 'src/user/user.service';
Expand Down Expand Up @@ -84,6 +84,13 @@ export class ChatService {
}
}

removeUserFromRoom(roomId: number, userId: number) {
const client = this.clients.get(userId);
if (client) {
client.leave(roomId.toString());
}
}

getUsersBlockedBy(userId: number) {
return this.prisma.user
.findUniqueOrThrow({
Expand All @@ -96,12 +103,10 @@ export class ChatService {
@OnEvent('room.created', { async: true })
async handleRoomCreatedEvent(event: RoomCreatedEvent) {
await this.addUserToRoom(event.roomId, event.userId);
}

async removeUserFromRoom(event: RoomLeftEvent) {
const client = this.clients.get(event.userId);
if (client) {
client.leave(event.roomId.toString());
if (event.userIds) {
event.userIds.forEach((userId) =>
this.addUserToRoom(event.roomId, userId),
);
}
}

Expand Down Expand Up @@ -131,10 +136,10 @@ export class ChatService {
});
}

deleteRoom(roomId: number) {
roomId;
// TODO: delete room
// this.server.socketsLeave(roomId.toString());
deleteSocketRoom(event: RoomDeletedEvent) {
event.userIds.forEach((userId) =>
this.removeUserFromRoom(event.roomId, userId),
);
}

sendToRoom(roomId: number, event: string, data: any) {
Expand Down
1 change: 1 addition & 0 deletions backend/src/common/events/room-created.event.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export class RoomCreatedEvent {
roomId: number;
userId: number;
userIds?: number[];
}
5 changes: 5 additions & 0 deletions backend/src/common/events/room-deleted.event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class RoomDeletedEvent {
roomId: number;
userIds: number[];
accessLevel: 'PUBLIC' | 'PRIVATE' | 'PROTECTED' | 'DIRECT';
}
19 changes: 16 additions & 3 deletions backend/src/room/room.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Role, User } from '@prisma/client';
import { RoomCreatedEvent } from 'src/common/events/room-created.event';
import { RoomDeletedEvent } from 'src/common/events/room-deleted.event';
import { RoomEnteredEvent } from 'src/common/events/room-entered.event';
import { RoomLeftEvent } from 'src/common/events/room-left.event';
import { RoomUpdateRoleEvent } from 'src/common/events/room-update-role.event';
Expand Down Expand Up @@ -65,6 +66,7 @@ export class RoomService {
const event: RoomCreatedEvent = {
roomId: room.id,
userId: user.id,
userIds,
};
this.eventEmitter.emit('room.created', event);
return room;
Expand Down Expand Up @@ -197,10 +199,19 @@ export class RoomService {
});
}

removeRoom(roomId: number): Promise<RoomEntity> {
return this.prisma.room.delete({
async removeRoom(roomId: number): Promise<RoomEntity> {
const users = await this.findAllUserOnRoom(roomId);
const deletedRoom = await this.prisma.room.delete({
where: { id: roomId },
});
const memberIds = users.map((member) => member.userId);
const event: RoomDeletedEvent = {
roomId: roomId,
userIds: memberIds,
accessLevel: deletedRoom.accessLevel,
};
this.eventEmitter.emit('room.delete', event);
return deletedRoom;
}

// UserOnRoom CRUD
Expand Down Expand Up @@ -316,7 +327,9 @@ export class RoomService {
},
},
});
// TODO: If owner leaves the room, the room should be deleted or a new owner should be assigned
if (deletedUserOnRoom.role === Role.OWNER) {
await this.removeRoom(roomId);
}
const event: RoomLeftEvent = {
roomId: roomId,
userId: userId,
Expand Down
156 changes: 156 additions & 0 deletions backend/test/chat-gateway.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AppModule } from 'src/app.module';
import { MessageEntity } from 'src/chat/entities/message.entity';
import { constants } from './constants';
import { TestApp, UserEntityWithAccessToken } from './utils/app';
import { CreateRoomDto } from 'src/room/dto/create-room.dto';

async function createNestApp(): Promise<INestApplication> {
const moduleFixture: TestingModule = await Test.createTestingModule({
Expand Down Expand Up @@ -1148,6 +1149,161 @@ describe('ChatGateway and ChatController (e2e)', () => {
});
});
});

describe('Notification that deleted a room', () => {
const setupRoom = async (createRoomDto: CreateRoomDto) => {
const dto = { ...createRoomDto, userIds: [user2.id] };
const res = await app.createRoom(dto, user1.accessToken).expect(201);
const room = res.body;
expect(room.id).toBeDefined();
return room;
};

describe('Notification that deleted a public room', () => {
let _publicRoom;
let ctx11: Promise<void[]>;
beforeAll(async () => {
_publicRoom = await setupRoom(constants.room.publicRoom);

const expectedEvent = {
roomId: _publicRoom.id,
};
const promises = [ws1, ws2, ws3, ws4, ws5, ws6].map(
(ws) =>
new Promise<void>((resolve) => {
ws.on('delete-room', (data) => {
expect(data).toEqual(expectedEvent);
ws.off('delete-room');
resolve();
});
}),
);
ctx11 = Promise.all(promises);
});
afterAll(async () => {
await app.deleteRoom(_publicRoom.id, user1.accessToken);
lim396 marked this conversation as resolved.
Show resolved Hide resolved
});
it('is sent when user1 deletes the public room', async () => {
await app.deleteRoom(_publicRoom.id, user1.accessToken).expect(204);
});
it('should be received by all users', async () => {
await ctx11;
});
});

describe('Notification that deleted a protected room', () => {
let _protectedRoom;
let ctx12: Promise<void[]>;
beforeAll(async () => {
_protectedRoom = await setupRoom(constants.room.protectedRoom);

const expectedEvent = {
roomId: _protectedRoom.id,
};
const promises = [ws1, ws2, ws3, ws4, ws5, ws6].map(
(ws) =>
new Promise<void>((resolve) => {
ws.on('delete-room', (data) => {
expect(data).toEqual(expectedEvent);
ws.off('delete-room');
resolve();
});
}),
);
ctx12 = Promise.all(promises);
});
afterAll(async () => {
await app.deleteRoom(_protectedRoom.id, user1.accessToken);
});
it('is sent when user1 deletes the protected room', async () => {
await app
.deleteRoom(_protectedRoom.id, user1.accessToken)
.expect(204);
});
it('should be received by all users', async () => {
await ctx12;
});
});

describe('Notification that deleted a private room', () => {
let _privateRoom;
let ctx13: Promise<void[]>;
beforeAll(async () => {
_privateRoom = await setupRoom(constants.room.privateRoom);

const expectedEvent = {
roomId: _privateRoom.id,
};
const promises = [ws1, ws2].map(
(ws) =>
new Promise<void>((resolve) => {
ws.on('delete-room', (data) => {
expect(data).toEqual(expectedEvent);
ws.off('delete-room');
resolve();
});
}),
);
ctx13 = Promise.all(promises);
});
afterAll(async () => {
await app.deleteRoom(_privateRoom.id, user1.accessToken);
});
it('is sent when user1 deletes the private room', async () => {
await app.deleteRoom(_privateRoom.id, user1.accessToken).expect(204);
});
it('should be received by room members', async () => {
await ctx13;
});
it('should not be received by non-members', (done) => {
const mockDeleteRoomEventListener = jest.fn();
ws3.on('delete-room', mockDeleteRoomEventListener);
setTimeout(() => {
expect(mockDeleteRoomEventListener).not.toBeCalled();
ws3.off('delete-room');
done();
}, waitTime);
});
});
});
lim396 marked this conversation as resolved.
Show resolved Hide resolved

describe('Notification that owner has left the room (delete-room)', () => {
let room;
let ctx14: Promise<void[]>;
const setupRoom = async (createRoomDto: CreateRoomDto) => {
const dto = { ...createRoomDto, userIds: [user2.id] };
const res = await app.createRoom(dto, user1.accessToken).expect(201);
const room = res.body;
expect(room.id).toBeDefined();
return room;
};
beforeAll(async () => {
room = await setupRoom(constants.room.publicRoom);
const expectedEvent = {
roomId: room.id,
};
const promises = [ws1, ws2, ws3, ws4, ws5, ws6].map(
(ws) =>
new Promise<void>((resolve) => {
ws.on('delete-room', (data) => {
expect(data).toEqual(expectedEvent);
ws.off('delete-room');
resolve();
});
}),
);
ctx14 = Promise.all(promises);
});
afterAll(async () => {
await app.deleteRoom(room.id, user1.accessToken);
});
it('is sent when owner(user1) leaves the public room', async () => {
await app.leaveRoom(room.id, user1.accessToken).expect(204);
});
it('should be received by all users', async () => {
await ctx14;
});
});
});

/*
Expand Down
12 changes: 11 additions & 1 deletion backend/test/room.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -966,7 +966,6 @@ describe('RoomController (e2e)', () => {
describe('owner', () => {
beforeAll(setupRooms);
afterAll(teardownRooms);
// TODO: What if owner leaves the room?
test('should leave public room (204 No Content)', async () => {
await app.leaveRoom(_publicRoom.id, owner.accessToken).expect(204);
});
Expand All @@ -976,6 +975,17 @@ describe('RoomController (e2e)', () => {
test('should leave protected room (204 No Content)', async () => {
await app.leaveRoom(_protectedRoom.id, owner.accessToken).expect(204);
});
test('should not get public room after owner leaves (404 Not Found)', async () => {
return app.getRoom(_publicRoom.id, owner.accessToken).expect(404);
});
test('should not get private room after owner leaves (404 Not Found)', async () => {
await app.getRoom(_privateRoom.id, member.accessToken).expect(404);
await app.getRoom(_privateRoom.id, admin.accessToken).expect(404);
return app.getRoom(_privateRoom.id, owner.accessToken).expect(404);
});
test('should not get protected room after owner leaves (404 Not Found)', async () => {
return app.getRoom(_protectedRoom.id, owner.accessToken).expect(404);
});
});
describe('admin', () => {
beforeAll(setupRooms);
Expand Down
Loading