-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
493 additions
and
1 deletion.
There are no files selected for viewing
14 changes: 14 additions & 0 deletions
14
src/app/application/group/command/changeGroupState/ChangeGroupStateCommand.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { GroupState } from '@sight/app/domain/group/model/constant'; | ||
|
||
type ChangeGroupStateRequester = { | ||
userId: string; | ||
isManager: boolean; | ||
}; | ||
|
||
export class ChangeGroupStateCommand { | ||
constructor( | ||
readonly requester: ChangeGroupStateRequester, | ||
readonly groupId: string, | ||
readonly nextState: GroupState, | ||
) {} | ||
} |
191 changes: 191 additions & 0 deletions
191
src/app/application/group/command/changeGroupState/ChangeGroupStateCommandHandler.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
import { Test } from '@nestjs/testing'; | ||
import { advanceTo, clear } from 'jest-date-mock'; | ||
|
||
import { ChangeGroupStateCommand } from '@sight/app/application/group/command/changeGroupState/ChangeGroupStateCommand'; | ||
import { ChangeGroupStateCommandHandler } from '@sight/app/application/group/command/changeGroupState/ChangeGroupStateCommandHandler'; | ||
import { ChangeGroupStateCommandResult } from '@sight/app/application/group/command/changeGroupState/ChangeGroupStateCommandResult'; | ||
|
||
import { GroupLogFactory } from '@sight/app/domain/group/GroupLogFactory'; | ||
import { GroupState } from '@sight/app/domain/group/model/constant'; | ||
import { Group } from '@sight/app/domain/group/model/Group'; | ||
import { | ||
GroupLogRepository, | ||
IGroupLogRepository, | ||
} from '@sight/app/domain/group/IGroupLogRepository'; | ||
import { | ||
GroupRepository, | ||
IGroupRepository, | ||
} from '@sight/app/domain/group/IGroupRepository'; | ||
|
||
import { DomainFixture } from '@sight/__test__/fixtures'; | ||
import { generateEmptyProviders } from '@sight/__test__/util'; | ||
import { Message } from '@sight/constant/message'; | ||
|
||
describe('ChangeGroupStateCommandHandler', () => { | ||
let handler: ChangeGroupStateCommandHandler; | ||
let groupRepository: jest.Mocked<IGroupRepository>; | ||
let groupLogFactory: jest.Mocked<GroupLogFactory>; | ||
let groupLogRepository: jest.Mocked<IGroupLogRepository>; | ||
|
||
beforeAll(async () => { | ||
advanceTo(new Date()); | ||
|
||
const testModule = await Test.createTestingModule({ | ||
providers: [ | ||
ChangeGroupStateCommandHandler, | ||
...generateEmptyProviders( | ||
GroupRepository, | ||
GroupLogFactory, | ||
GroupLogRepository, | ||
), | ||
], | ||
}).compile(); | ||
|
||
handler = testModule.get(ChangeGroupStateCommandHandler); | ||
groupRepository = testModule.get(GroupRepository); | ||
groupLogFactory = testModule.get(GroupLogFactory); | ||
groupLogRepository = testModule.get(GroupLogRepository); | ||
}); | ||
|
||
afterAll(() => { | ||
clear(); | ||
}); | ||
|
||
describe('execute', () => { | ||
let group: Group; | ||
|
||
const requesterUserId = 'requester-user-id'; | ||
const groupId = 'group-id'; | ||
const nextState = GroupState.END_SUCCESS; | ||
|
||
beforeEach(() => { | ||
group = DomainFixture.generateGroup({ | ||
adminUserId: requesterUserId, | ||
}); | ||
const log = DomainFixture.generateGroupLog(); | ||
|
||
group.isCustomerServiceGroup = jest.fn().mockReturnValue(false); | ||
group.isPracticeGroup = jest.fn().mockReturnValue(false); | ||
groupLogFactory.create = jest.fn().mockReturnValue(log); | ||
groupRepository.findById = jest.fn().mockResolvedValue(group); | ||
groupLogRepository.nextId = jest.fn().mockReturnValue('some-id'); | ||
|
||
group.changeState = jest.fn(); | ||
groupRepository.save = jest.fn(); | ||
groupLogRepository.save = jest.fn(); | ||
}); | ||
|
||
test('그룹이 존재하지 않으면 예외를 발생시켜야 한다', async () => { | ||
groupRepository.findById = jest.fn().mockResolvedValue(null); | ||
|
||
const command = new ChangeGroupStateCommand( | ||
{ userId: requesterUserId, isManager: false }, | ||
groupId, | ||
nextState, | ||
); | ||
await expect(handler.execute(command)).rejects.toThrowError( | ||
Message.GROUP_NOT_FOUND, | ||
); | ||
}); | ||
|
||
test('고객센터 그룹의 상태를 수정하려 하면 예외를 발생시켜야 한다', async () => { | ||
group.isCustomerServiceGroup = jest.fn().mockReturnValue(true); | ||
|
||
const command = new ChangeGroupStateCommand( | ||
{ userId: requesterUserId, isManager: false }, | ||
groupId, | ||
nextState, | ||
); | ||
await expect(handler.execute(command)).rejects.toThrowError( | ||
Message.GROUP_NOT_EDITABLE, | ||
); | ||
}); | ||
|
||
test('그룹 활용 실습 그룹의 상태를 수정하려 하면 예외를 발생시켜야 한다', async () => { | ||
group.isPracticeGroup = jest.fn().mockReturnValue(true); | ||
|
||
const command = new ChangeGroupStateCommand( | ||
{ userId: requesterUserId, isManager: false }, | ||
groupId, | ||
nextState, | ||
); | ||
await expect(handler.execute(command)).rejects.toThrowError( | ||
Message.GROUP_NOT_EDITABLE, | ||
); | ||
}); | ||
|
||
test('그룹을 저장하고 변경된 상태를 반환해야 한다', async () => { | ||
const command = new ChangeGroupStateCommand( | ||
{ userId: requesterUserId, isManager: false }, | ||
groupId, | ||
nextState, | ||
); | ||
const expected = new ChangeGroupStateCommandResult(nextState); | ||
|
||
const result = await handler.execute(command); | ||
|
||
expect(groupRepository.save).toBeCalledTimes(1); | ||
expect(groupRepository.save).toBeCalledWith(group); | ||
|
||
expect(result).toEqual(expected); | ||
}); | ||
|
||
describe('일반 유저가 요청했을 때', () => { | ||
const isManager = false; | ||
|
||
test('요청자가 그룹의 관리자가 아니라면 예외를 발생시켜야 한다', async () => { | ||
const otherUserId = 'other-user-id'; | ||
|
||
const command = new ChangeGroupStateCommand( | ||
{ userId: otherUserId, isManager }, | ||
groupId, | ||
nextState, | ||
); | ||
await expect(handler.execute(command)).rejects.toThrowError( | ||
Message.ONLY_GROUP_ADMIN_CAN_EDIT_GROUP, | ||
); | ||
}); | ||
|
||
test('그룹을 중단 처리하려 하면 예외를 발생시켜야 한다', async () => { | ||
const command = new ChangeGroupStateCommand( | ||
{ userId: requesterUserId, isManager }, | ||
groupId, | ||
GroupState.SUSPEND, | ||
); | ||
await expect(handler.execute(command)).rejects.toThrowError( | ||
Message.ONLY_MANAGER_CAN_SUSPEND_GROUP, | ||
); | ||
}); | ||
}); | ||
|
||
describe('관리자가 요청했을 때', () => { | ||
const isManager = true; | ||
|
||
test('요청자가 그룹의 관리자가 아니더라도 상태를 변경할 수 있어야 한다', async () => { | ||
const otherManagerUserId = 'other-manager-user-id'; | ||
|
||
const command = new ChangeGroupStateCommand( | ||
{ userId: otherManagerUserId, isManager }, | ||
groupId, | ||
nextState, | ||
); | ||
await handler.execute(command); | ||
|
||
expect(group.changeState).toBeCalledTimes(1); | ||
expect(group.changeState).toBeCalledWith(nextState); | ||
}); | ||
|
||
test('그룹을 중단시킬 수 있어야 한다', async () => { | ||
const command = new ChangeGroupStateCommand( | ||
{ userId: requesterUserId, isManager }, | ||
groupId, | ||
GroupState.SUSPEND, | ||
); | ||
await handler.execute(command); | ||
|
||
expect(group.changeState).toBeCalledTimes(1); | ||
expect(group.changeState).toBeCalledWith(GroupState.SUSPEND); | ||
}); | ||
}); | ||
}); | ||
}); |
93 changes: 93 additions & 0 deletions
93
src/app/application/group/command/changeGroupState/ChangeGroupStateCommandHandler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; | ||
import { | ||
ForbiddenException, | ||
Inject, | ||
NotFoundException, | ||
UnprocessableEntityException, | ||
} from '@nestjs/common'; | ||
|
||
import { Transactional } from '@sight/core/persistence/transaction/Transactional'; | ||
|
||
import { ChangeGroupStateCommand } from '@sight/app/application/group/command/changeGroupState/ChangeGroupStateCommand'; | ||
import { ChangeGroupStateCommandResult } from '@sight/app/application/group/command/changeGroupState/ChangeGroupStateCommandResult'; | ||
|
||
import { GroupLogFactory } from '@sight/app/domain/group/GroupLogFactory'; | ||
import { GroupState } from '@sight/app/domain/group/model/constant'; | ||
import { | ||
GroupLogRepository, | ||
IGroupLogRepository, | ||
} from '@sight/app/domain/group/IGroupLogRepository'; | ||
import { | ||
GroupRepository, | ||
IGroupRepository, | ||
} from '@sight/app/domain/group/IGroupRepository'; | ||
|
||
import { Message } from '@sight/constant/message'; | ||
|
||
@CommandHandler(ChangeGroupStateCommand) | ||
export class ChangeGroupStateCommandHandler | ||
implements | ||
ICommandHandler<ChangeGroupStateCommand, ChangeGroupStateCommandResult> | ||
{ | ||
constructor( | ||
@Inject(GroupRepository) | ||
private readonly groupRepository: IGroupRepository, | ||
@Inject(GroupLogFactory) | ||
private readonly groupLogFactory: GroupLogFactory, | ||
@Inject(GroupLogRepository) | ||
private readonly groupLogRepository: IGroupLogRepository, | ||
) {} | ||
|
||
@Transactional() | ||
async execute( | ||
command: ChangeGroupStateCommand, | ||
): Promise<ChangeGroupStateCommandResult> { | ||
const { requester, groupId, nextState } = command; | ||
|
||
const group = await this.groupRepository.findById(groupId); | ||
if (!group) { | ||
throw new NotFoundException(Message.GROUP_NOT_FOUND); | ||
} | ||
|
||
if (group.isCustomerServiceGroup() || group.isPracticeGroup()) { | ||
throw new UnprocessableEntityException(Message.GROUP_NOT_EDITABLE); | ||
} | ||
|
||
if (group.adminUserId !== requester.userId && !requester.isManager) { | ||
throw new ForbiddenException(Message.ONLY_GROUP_ADMIN_CAN_EDIT_GROUP); | ||
} | ||
|
||
if (nextState === GroupState.SUSPEND && !requester.isManager) { | ||
throw new ForbiddenException(Message.ONLY_MANAGER_CAN_SUSPEND_GROUP); | ||
} | ||
|
||
group.changeState(nextState); | ||
group.wake(); | ||
await this.groupRepository.save(group); | ||
|
||
const newGroupLog = this.groupLogFactory.create({ | ||
id: this.groupLogRepository.nextId(), | ||
groupId, | ||
userId: requester.userId, | ||
message: this.buildMessage(nextState), | ||
}); | ||
await this.groupLogRepository.save(newGroupLog); | ||
|
||
return new ChangeGroupStateCommandResult(nextState); | ||
} | ||
|
||
private buildMessage(nextState: GroupState): string { | ||
switch (nextState) { | ||
case 'PROGRESS': | ||
return '그룹이 진행 중입니다.'; | ||
case 'END_SUCCESS': | ||
return '그룹이 종료(성공)되었습니다.'; | ||
case 'END_FAIL': | ||
return '그룹이 종료(실패)되었습니다.'; | ||
case 'SUSPEND': | ||
return '그룹이 중단 처리되었습니다.'; | ||
case 'PENDING': | ||
return 'not reachable'; | ||
} | ||
} | ||
} |
5 changes: 5 additions & 0 deletions
5
src/app/application/group/command/changeGroupState/ChangeGroupStateCommandResult.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { GroupState } from '@sight/app/domain/group/model/constant'; | ||
|
||
export class ChangeGroupStateCommandResult { | ||
constructor(readonly nextState: GroupState) {} | ||
} |
Oops, something went wrong.