Skip to content

Commit

Permalink
feat: 그룹 포트폴리오 비활성화 구현 (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
Coalery authored Jan 16, 2024
1 parent 7c5a5a2 commit 79cd113
Show file tree
Hide file tree
Showing 16 changed files with 474 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ICommand } from '@nestjs/cqrs';

export class DisablePortfolioCommand implements ICommand {
constructor(
readonly groupId: string,
readonly requesterUserId: string,
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Test } from '@nestjs/testing';
import { advanceTo, clear } from 'jest-date-mock';

import { DisablePortfolioCommand } from '@sight/app/application/group/command/disablePortfolio/DisablePortfolioCommand';
import { DisablePortfolioCommandHandler } from '@sight/app/application/group/command/disablePortfolio/DisablePortfolioCommandHandler';

import { Group } from '@sight/app/domain/group/model/Group';
import {
GroupRepository,
IGroupRepository,
} from '@sight/app/domain/group/IGroupRepository';

import { DomainFixture } from '@sight/__test__/fixtures';
import { Message } from '@sight/constant/message';

describe('DisablePortfolioCommandHandler', () => {
let handler: DisablePortfolioCommandHandler;
let groupRepository: jest.Mocked<IGroupRepository>;

beforeAll(async () => {
advanceTo(new Date());

const testModule = await Test.createTestingModule({
providers: [
DisablePortfolioCommandHandler,
{ provide: GroupRepository, useValue: {} },
],
}).compile();

handler = testModule.get(DisablePortfolioCommandHandler);
groupRepository = testModule.get(GroupRepository);
});

afterAll(() => {
clear();
});

describe('handle', () => {
let group: Group;

const groupId = 'groupId';
const groupAdminUserId = 'groupAdminUserId';

beforeEach(() => {
group = DomainFixture.generateGroup({
adminUserId: groupAdminUserId,
hasPortfolio: true,
});

groupRepository.findById = jest.fn().mockResolvedValue(group);

groupRepository.save = jest.fn();
});

test('그룹이 존재하지 않는다면 예외를 발생시켜야 한다', async () => {
groupRepository.findById = jest.fn().mockResolvedValue(null);

await expect(
handler.execute(new DisablePortfolioCommand(groupId, groupAdminUserId)),
).rejects.toThrowError(Message.GROUP_NOT_FOUND);
});

test('요청자가 그룹장이 아니라면 예외를 발생시켜야 한다', async () => {
const otherUserId = 'otherUserId';

await expect(
handler.execute(new DisablePortfolioCommand(groupId, otherUserId)),
).rejects.toThrowError(Message.ONLY_GROUP_ADMIN_CAN_EDIT_GROUP);
});

test('그룹이 고객센터 그룹이라면 예외를 발생시켜야 한다', async () => {
jest.spyOn(group, 'isCustomerServiceGroup').mockReturnValue(true);

await expect(
handler.execute(new DisablePortfolioCommand(groupId, groupAdminUserId)),
).rejects.toThrowError(Message.CANNOT_MODIFY_CUSTOMER_SERVICE_GROUP);
});

test('그룹의 포트폴리오를 비활성화 시켜야 한다', async () => {
jest.spyOn(group, 'disablePortfolio');

await handler.execute(
new DisablePortfolioCommand(groupId, groupAdminUserId),
);

expect(group.disablePortfolio).toBeCalled();
});

test('그룹을 저장해야 한다', async () => {
await handler.execute(
new DisablePortfolioCommand(groupId, groupAdminUserId),
);

expect(groupRepository.save).toBeCalledWith(group);
expect(groupRepository.save).toBeCalledTimes(1);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import {
ForbiddenException,
Inject,
NotFoundException,
UnprocessableEntityException,
} from '@nestjs/common';

import { DisablePortfolioCommand } from '@sight/app/application/group/command/disablePortfolio/DisablePortfolioCommand';

import {
GroupRepository,
IGroupRepository,
} from '@sight/app/domain/group/IGroupRepository';

import { Message } from '@sight/constant/message';

@CommandHandler(DisablePortfolioCommand)
export class DisablePortfolioCommandHandler
implements ICommandHandler<DisablePortfolioCommand>
{
constructor(
@Inject(GroupRepository)
private readonly groupRepository: IGroupRepository,
) {}

async execute(command: DisablePortfolioCommand): Promise<void> {
const { groupId, requesterUserId } = command;

const group = await this.groupRepository.findById(groupId);
if (!group) {
throw new NotFoundException(Message.GROUP_NOT_FOUND);
}

if (group.adminUserId !== requesterUserId) {
throw new ForbiddenException(Message.ONLY_GROUP_ADMIN_CAN_EDIT_GROUP);
}

if (group.isCustomerServiceGroup()) {
throw new UnprocessableEntityException(
Message.CANNOT_MODIFY_CUSTOMER_SERVICE_GROUP,
);
}

group.disablePortfolio();
await this.groupRepository.save(group);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ export class GroupCreatedHandler implements IEventHandler<GroupCreated> {
await this.userRepository.save(authorUser);

this.slackSender.send({
sourceUserId: null,
targetUserId: group.authorUserId,
category: SlackMessageCategory.GROUP_ACTIVITY,
message: `${group.title} 그룹을 만들었습니다.`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { Test } from '@nestjs/testing';
import { advanceTo, clear } from 'jest-date-mock';
import { ClsService } from 'nestjs-cls';

import { IRequester } from '@sight/core/auth/IRequester';
import { UserRole } from '@sight/core/auth/UserRole';
import { MessageBuilder } from '@sight/core/message/MessageBuilder';

import { GroupPortfolioDisabledHandler } from '@sight/app/application/group/eventHandler/GroupPortfolioDisabledHandler';

import { PRACTICE_GROUP_ID } from '@sight/app/domain/group/model/constant';
import { Group } from '@sight/app/domain/group/model/Group';
import {
ISlackSender,
SlackSender,
} from '@sight/app/domain/adapter/ISlackSender';
import {
GroupLogger,
IGroupLogger,
} from '@sight/app/domain/group/IGroupLogger';
import {
GroupMemberRepository,
IGroupMemberRepository,
} from '@sight/app/domain/group/IGroupMemberRepository';
import {
GroupRepository,
IGroupRepository,
} from '@sight/app/domain/group/IGroupRepository';
import {
IUserRepository,
UserRepository,
} from '@sight/app/domain/user/IUserRepository';

import { DomainFixture } from '@sight/__test__/fixtures';
import { Point } from '@sight/constant/point';

describe('GroupPortfolioDisabledHandler', () => {
let handler: GroupPortfolioDisabledHandler;
let messageBuilder: MessageBuilder;
let clsService: jest.Mocked<ClsService>;
let groupRepository: jest.Mocked<IGroupRepository>;
let groupMemberRepository: jest.Mocked<IGroupMemberRepository>;
let userRepository: jest.Mocked<IUserRepository>;
let groupLogger: jest.Mocked<IGroupLogger>;
let slackSender: jest.Mocked<ISlackSender>;

beforeAll(async () => {
advanceTo(new Date());

const testModule = await Test.createTestingModule({
providers: [
GroupPortfolioDisabledHandler,
MessageBuilder,
{ provide: ClsService, useValue: {} },
{ provide: GroupRepository, useValue: {} },
{ provide: GroupMemberRepository, useValue: {} },
{ provide: UserRepository, useValue: {} },
{ provide: GroupLogger, useValue: {} },
{ provide: SlackSender, useValue: {} },
],
}).compile();

handler = testModule.get(GroupPortfolioDisabledHandler);
messageBuilder = testModule.get(MessageBuilder);
clsService = testModule.get(ClsService);
groupRepository = testModule.get(GroupRepository);
groupMemberRepository = testModule.get(GroupMemberRepository);
userRepository = testModule.get(UserRepository);
groupLogger = testModule.get(GroupLogger);
slackSender = testModule.get(SlackSender);
});

afterAll(() => {
clear();
});

describe('handle', () => {
let group: Group;

const groupId = 'groupId';
const requester: IRequester = {
userId: 'requesterUserId',
role: UserRole.USER,
};

beforeEach(() => {
group = DomainFixture.generateGroup();

clsService.get = jest.fn().mockReturnValue(requester);
groupRepository.findById = jest.fn().mockResolvedValue(group);
groupMemberRepository.findByGroupId = jest.fn().mockResolvedValue([]);
userRepository.findByIds = jest.fn().mockResolvedValue([]);

jest.spyOn(messageBuilder, 'build');
groupLogger.log = jest.fn();
userRepository.save = jest.fn();
slackSender.send = jest.fn();
});

test('그룹이 존재하지 않으면 아무 동작도 하지 않아야 한다', async () => {
groupRepository.findById = jest.fn().mockResolvedValue(null);

await handler.handle({ groupId });

expect(groupRepository.findById).toBeCalledWith(groupId);
});

test('그룹 로그를 남겨야 한다', async () => {
await handler.handle({ groupId });

expect(groupLogger.log).toBeCalledTimes(1);
});

test('모든 그룹 멤버들에게서 포인트를 회수해야 한다', async () => {
const user = DomainFixture.generateUser();
const groupMember = DomainFixture.generateGroupMember({
userId: user.id,
});

groupMemberRepository.findByGroupId = jest
.fn()
.mockResolvedValue([groupMember]);
userRepository.findByIds = jest.fn().mockResolvedValue([user]);
jest.spyOn(user, 'grantPoint');

await handler.handle({ groupId });

expect(user.grantPoint).toBeCalledTimes(1);
expect(user.grantPoint).toBeCalledWith(
-Point.GROUP_ENABLED_PORTFOLIO,
expect.any(String),
);

expect(userRepository.save).toBeCalledTimes(1);
expect(userRepository.save).toBeCalledWith(user);
});

test('그룹 활용 실습 그룹이면 포인트를 회수하지 않아야 한다', async () => {
const practiceGroup = DomainFixture.generateGroup({
id: PRACTICE_GROUP_ID,
});
groupRepository.findById = jest.fn().mockResolvedValue(practiceGroup);

await handler.handle({ groupId });

expect(userRepository.save).not.toBeCalled();
});

test('메시지를 보내야 한다', async () => {
await handler.handle({ groupId });

expect(slackSender.send).toBeCalledTimes(1);
});
});
});
Loading

0 comments on commit 79cd113

Please sign in to comment.