diff --git a/backend/src/project/dto/setting-page/CreateJoinRequestNotify.dto.ts b/backend/src/project/dto/setting-page/CreateJoinRequestNotify.dto.ts new file mode 100644 index 00000000..908d4024 --- /dev/null +++ b/backend/src/project/dto/setting-page/CreateJoinRequestNotify.dto.ts @@ -0,0 +1,48 @@ +import { Member } from 'src/member/entity/member.entity'; +import { ProjectJoinRequest } from 'src/project/entity/project-join-request.entity'; + +class JoinRequestDto { + id: number; + memberId: number; + username: string; + imageUrl: string; + + static of( + projectJoinRequest: ProjectJoinRequest, + member: Member, + ): JoinRequestDto { + const dto = new JoinRequestDto(); + dto.id = projectJoinRequest.id; + dto.memberId = member.id; + dto.username = member.username; + dto.imageUrl = member.github_image_url; + return dto; + } +} + +class ContentDto { + joinRequest: JoinRequestDto; + + static of(joinRequest: ProjectJoinRequest, member: Member): ContentDto { + const dto = new ContentDto(); + dto.joinRequest = JoinRequestDto.of(joinRequest, member); + return dto; + } +} + +export class CreateJoinRequestNotifyDto { + domain: string; + action: string; + content: ContentDto; + + static of( + joinRequest: ProjectJoinRequest, + member: Member, + ): CreateJoinRequestNotifyDto { + const dto = new CreateJoinRequestNotifyDto(); + dto.domain = 'joinRequest'; + dto.action = 'create'; + dto.content = ContentDto.of(joinRequest, member); + return dto; + } +} diff --git a/backend/src/project/project.controller.ts b/backend/src/project/project.controller.ts index 640108f3..834aea68 100644 --- a/backend/src/project/project.controller.ts +++ b/backend/src/project/project.controller.ts @@ -93,8 +93,13 @@ export class ProjectController { @Res() response: Response, ) { try { - await this.projectService.createProjectJoinRequest( - body.inviteLinkId, + const projectJoinRequest = + await this.projectService.createProjectJoinRequest( + body.inviteLinkId, + request.member, + ); + this.projectWebsocketGateway.notifyCreateJoinRequestToSettingPage( + projectJoinRequest, request.member, ); } catch (e) { diff --git a/backend/src/project/service/project.service.ts b/backend/src/project/service/project.service.ts index 9596427e..9b10543f 100644 --- a/backend/src/project/service/project.service.ts +++ b/backend/src/project/service/project.service.ts @@ -154,7 +154,7 @@ export class ProjectService { async createProjectJoinRequest( inviteLinkId: string, member: Member, - ): Promise { + ): Promise { const project = await this.getProjectByLinkId(inviteLinkId); if (!project) throw new Error('Project not found'); const isProjectMember = await this.isProjectMember(project.id, member); @@ -164,9 +164,11 @@ export class ProjectService { project.id, member.id, ); - await this.projectRepository.createProjectJoinRequest( - newProjectJoinRequest, - ); + const projectJoinRequest = + await this.projectRepository.createProjectJoinRequest( + newProjectJoinRequest, + ); + return projectJoinRequest; } catch (e) { if (e.message === 'DUPLICATED PROJECT ID AND MEMBER ID') { throw new Error('Join request already submitted'); diff --git a/backend/src/project/websocket.gateway.ts b/backend/src/project/websocket.gateway.ts index 13c59096..d4183678 100644 --- a/backend/src/project/websocket.gateway.ts +++ b/backend/src/project/websocket.gateway.ts @@ -23,6 +23,8 @@ import { WsProjectStoryController } from './ws-controller/ws-project-story.contr import { WsProjectTaskController } from './ws-controller/ws-project-task.controller'; import { WsProjectInfoController } from './ws-controller/ws-project-info.controller'; import { WsProjectInviteLinkController } from './ws-controller/ws-project-invite-link.controller'; +import { ProjectJoinRequest } from './entity/project-join-request.entity'; +import { CreateJoinRequestNotifyDto } from './dto/setting-page/CreateJoinRequestNotify.dto'; @WebSocketGateway({ namespace: /project-\d+/, @@ -212,6 +214,22 @@ export class ProjectWebsocketGateway this.namespaceMap.delete(projectId); } + notifyCreateJoinRequestToSettingPage( + projectJoinRequest: ProjectJoinRequest, + member: Member, + ) { + const projectNamespace = this.namespaceMap.get( + projectJoinRequest.projectId, + ); + if (!projectNamespace) return; + projectNamespace + .to('setting') + .emit( + 'setting', + CreateJoinRequestNotifyDto.of(projectJoinRequest, member), + ); + } + notifyJoinToConnectedMembers(projectId: number, member: Member) { const projectNamespace = this.namespaceMap.get(projectId); if (!projectNamespace) return; diff --git a/backend/test/project/ws-setting-page/ws-notify-project-join-request.e2e-spec.ts b/backend/test/project/ws-setting-page/ws-notify-project-join-request.e2e-spec.ts new file mode 100644 index 00000000..3c45f334 --- /dev/null +++ b/backend/test/project/ws-setting-page/ws-notify-project-join-request.e2e-spec.ts @@ -0,0 +1,112 @@ +import { Socket } from 'socket.io-client'; +import * as request from 'supertest'; +import { + app, + appInit, + connectServer, + createMember, + createProject, + getMemberByAccessToken, + getProjectLinkId, + listenAppAndSetPortEnv, + memberFixture, + memberFixture2, + projectPayload, +} from 'test/setup'; +import { Member } from 'src/member/entity/member.entity'; + +describe('WS Setting', () => { + beforeEach(async () => { + await app.close(); + await appInit(); + await listenAppAndSetPortEnv(app); + }); + + it('should notify join request in setting page when project join request is submitted', async () => { + const { accessToken: leaderAccessToken } = await createMember( + memberFixture, + app, + ); + + const { id: projectId } = await createProject( + leaderAccessToken, + projectPayload, + app, + ); + const leaderSocket = await enterSettingPage(projectId, leaderAccessToken); + + const { accessToken: requestingAccessToken } = await createMember( + memberFixture2, + app, + ); + + const requestingMember = await getMemberByAccessToken( + requestingAccessToken, + ); + + const expectPromise = expectNotifyJoinRequest( + leaderSocket, + requestingMember, + ); + const submitPromise = submitJoinRequest( + leaderAccessToken, + projectId, + requestingAccessToken, + ); + await Promise.all([expectPromise, submitPromise]); + closePage(leaderSocket); + }); + + async function enterSettingPage(projectId: number, accessToken: string) { + const socket = connectServer(projectId, accessToken); + socket.emit('joinSetting'); + + await new Promise((resolve) => { + socket.once('setting', (data) => { + const { action, domain } = data; + expect(domain).toBe('setting'); + expect(action).toBe('init'); + resolve(); + }); + }); + + return socket; + } + + function closePage(socket: Socket) { + socket.close(); + } +}); + +async function submitJoinRequest( + leaderAccessToken: string, + projectId: number, + requestingAccessToken: string, +): Promise { + const inviteLinkId = await getProjectLinkId(leaderAccessToken, projectId); + return request(app.getHttpServer()) + .post('/api/project/join-request') + .set('Authorization', `Bearer ${requestingAccessToken}`) + .send({ inviteLinkId }); +} + +async function expectNotifyJoinRequest( + socket: Socket, + requestingMember: Member, +) { + return new Promise((resolve) => { + socket.on('setting', (data) => { + const { action, domain, content } = data; + expect(domain).toBe('joinRequest'); + expect(action).toBe('create'); + expect(content.joinRequest).toBeDefined(); + expect(content.joinRequest.id).toBeDefined(); + expect(content.joinRequest.memberId).toBe(requestingMember.id); + expect(content.joinRequest.username).toBe(requestingMember.username); + expect(content.joinRequest.imageUrl).toBe( + requestingMember.github_image_url, + ); + resolve(); + }); + }); +}