Skip to content

Commit

Permalink
feat: 초대링크 변경 API 구현
Browse files Browse the repository at this point in the history
- 컨트롤러, 서비스, 레포지토리에서 초대링크 업데이트 메서드 구현
- 리더인 경우, 리더가 아닌경우 각각에 대한 초대링크 업데이트 E2E 테스트 추가
  • Loading branch information
choyoungwoo9 committed Sep 20, 2024
1 parent c9aa172 commit 3916432
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { IsNotEmpty, Matches } from 'class-validator';

export class InviteLinkUpdateRequestDto {
@Matches(/^update$/)
action: string;

@IsNotEmpty()
content: Record<string, any>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class inviteLinkDto {
inviteLinkId: string;

static of(inviteLinkId: string): inviteLinkDto {
const dto = new inviteLinkDto();
dto.inviteLinkId = inviteLinkId;
return dto;
}
}

export class InviteLinkUpdateResponseDto {
domain: string;
action: string;
content: inviteLinkDto;

static of(inviteLinkId: string): InviteLinkUpdateResponseDto {
const dto = new InviteLinkUpdateResponseDto();
dto.domain = 'inviteLink';
dto.action = 'update';
dto.content = inviteLinkDto.of(inviteLinkId);
return dto;
}
}
2 changes: 2 additions & 0 deletions backend/src/project/project.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { WsProjectStoryController } from './ws-controller/ws-project-story.contr
import { Task } from './entity/task.entity';
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';

@Module({
imports: [
Expand Down Expand Up @@ -53,6 +54,7 @@ import { WsProjectInfoController } from './ws-controller/ws-project-info.control
WsProjectStoryController,
WsProjectTaskController,
WsProjectInfoController,
WsProjectInviteLinkController,
],
})
export class ProjectModule {}
11 changes: 11 additions & 0 deletions backend/src/project/project.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ export class ProjectRepository {
});
}

async updateInviteLink(
projectId: number,
newInviteLinkId: string,
): Promise<boolean> {
const result = await this.projectRepository.update(
{ id: projectId },
{ inviteLinkId: newInviteLinkId },
);
return !!result.affected;
}

getProject(projectId: number): Promise<Project | null> {
return this.projectRepository.findOne({ where: { id: projectId } });
}
Expand Down
14 changes: 14 additions & 0 deletions backend/src/project/service/project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Story, StoryStatus } from '../entity/story.entity';
import { Task, TaskStatus } from '../entity/task.entity';
import { LexoRank } from 'lexorank';
import { MemberRole } from '../enum/MemberRole.enum';
import { v4 as uuidv4 } from 'uuid';

@Injectable()
export class ProjectService {
Expand Down Expand Up @@ -51,6 +52,19 @@ export class ProjectService {
return project;
}

async updateInviteLink(projectId: number, member: Member): Promise<string> {
if (!(await this.isProjectLeader(projectId, member))) {
throw new Error('Member is not the project leader');
}
const newInviteLinkId = await uuidv4();
const isUpdated = await this.projectRepository.updateInviteLink(
projectId,
newInviteLinkId,
);
if (!isUpdated) throw new Error('invite link not updated');
return newInviteLinkId;
}

async getProject(projectId: number, member: Member): Promise<Project | null> {
if (!(await this.isExistProject(projectId)))
throw new Error('Project not found');
Expand Down
12 changes: 12 additions & 0 deletions backend/src/project/websocket.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { WsProjectEpicController } from './ws-controller/ws-project-epic.control
import { WsProjectStoryController } from './ws-controller/ws-project-story.controller';
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';

@WebSocketGateway({
namespace: /project-\d+/,
Expand All @@ -42,6 +43,7 @@ export class ProjectWebsocketGateway
private readonly wsProjectStoryController: WsProjectStoryController,
private readonly wsProjectTaskController: WsProjectTaskController,
private readonly wsProjectInfoController: WsProjectInfoController,
private readonly wsProjectInviteLinkController: WsProjectInviteLinkController,
) {
this.namespaceMap = new Map();
}
Expand Down Expand Up @@ -132,6 +134,16 @@ export class ProjectWebsocketGateway
}
}

@SubscribeMessage('inviteLink')
async handleInviteLinkEvent(
@ConnectedSocket() client: ClientSocket,
@MessageBody() data: any,
) {
if (data.action === 'update') {
this.wsProjectInviteLinkController.updateInviteLink(client, data);
}
}

@SubscribeMessage('joinBacklog')
async handleJoinBacklogEvent(@ConnectedSocket() client: ClientSocket) {
this.wsProjectController.joinBacklogPage(client);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Injectable } from '@nestjs/common';
import { ProjectService } from '../service/project.service';
import { ClientSocket } from '../type/ClientSocket.type';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
import { getRecursiveErrorMsgList } from '../util/validation.util';
import { InviteLinkUpdateRequestDto } from '../dto/invite-link/InviteLinkUpdateRequest.dto';
import { InviteLinkUpdateResponseDto } from '../dto/invite-link/InviteLinkUpdateResponse.dto';

@Injectable()
export class WsProjectInviteLinkController {
constructor(private readonly projectService: ProjectService) {}
async updateInviteLink(client: ClientSocket, data: any) {
const errors = await validate(
plainToClass(InviteLinkUpdateRequestDto, data),
);
if (errors.length > 0) {
const errorList = getRecursiveErrorMsgList(errors);
client.emit('error', { errorList });
return;
}
try {
const newInviteLinkId = await this.projectService.updateInviteLink(
client.project.id,
client.member,
);
client.emit('landing', InviteLinkUpdateResponseDto.of(newInviteLinkId));
} catch (e) {
if (e.message === 'Member is not the project leader') {
client.disconnect(true);
} else throw e;
}
}
}
93 changes: 93 additions & 0 deletions backend/test/project/ws-landing-page/ws-invite-link.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {
app,
appInit,
connectServer,
createMember,
createProject,
getProjectLinkId,
joinProject,
listenAppAndSetPortEnv,
memberFixture,
memberFixture2,
projectPayload,
} from 'test/setup';
import {
emitJoinLanding,
handleConnectErrorWithReject,
handleErrorWithReject,
initLanding,
} from '../ws-common';

describe('WS invite link', () => {
beforeEach(async () => {
await app.close();
await appInit();
await listenAppAndSetPortEnv(app);
});
describe('update invite link', () => {
it('should return updated invite link data when project leader request', async () => {
let socket;
return new Promise<void>(async (resolve, reject) => {
const accessToken = (await createMember(memberFixture, app))
.accessToken;
const project = await createProject(accessToken, projectPayload, app);
socket = connectServer(project.id, accessToken);
handleConnectErrorWithReject(socket, reject);
handleErrorWithReject(socket, reject);
await emitJoinLanding(socket);
await initLanding(socket);
const data = {
action: 'update',
content: {},
};
socket.emit('inviteLink', data);
await expectUpdateInviteLink(socket);
resolve();
}).finally(() => {
socket.close();
});
});
const expectUpdateInviteLink = async (socket) => {
return await new Promise<void>((res) => {
socket.once('landing', async (data) => {
const { content, action, domain } = data;
expect(domain).toBe('inviteLink');
expect(action).toBe('update');
expect(content.inviteLinkId).toBeDefined();
expect(typeof content.inviteLinkId).toBe('string');
res();
});
});
};

it('should disconnect if the requester is not the project leader', async () => {
let socket;
return new Promise<void>(async (resolve, reject) => {
const accessToken = (await createMember(memberFixture, app))
.accessToken;
const project = await createProject(accessToken, projectPayload, app);
const projectLinkId = await getProjectLinkId(accessToken, project.id);

const accessToken2 = (await createMember(memberFixture2, app))
.accessToken;
await joinProject(accessToken2, projectLinkId);

socket = connectServer(project.id, accessToken2);
handleConnectErrorWithReject(socket, reject);
handleErrorWithReject(socket, reject);
await emitJoinLanding(socket);
await initLanding(socket);
const data = {
action: 'update',
content: {},
};
socket.emit('inviteLink', data);
socket.on('disconnect', () => {
resolve();
});
}).finally(() => {
socket.close();
});
});
});
});

0 comments on commit 3916432

Please sign in to comment.