Skip to content

Commit

Permalink
feat: 이미 투표한 토픽에 투표할 때 exception
Browse files Browse the repository at this point in the history
에외처리, 네이밍 구체화

Issue: #102
  • Loading branch information
melonturtle committed Jan 15, 2024
1 parent 67c5861 commit e8c2ac7
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 40 deletions.
4 changes: 4 additions & 0 deletions src/docs/asciidoc/topic.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ E2. 투표 시간을 미래 시간으로 요청

operation::topic-controller-test/vote-for-topic_voted-at-future_throw-exception[snippets="http-request,http-response"]

E3. 이미 투표한 토픽에 대해 투표

operation::topic-controller-test/vote-for-topic_duplicate-vote_throw-exception[snippets="http-request,http-response"]

### 1.8. 투표 수정

OK
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,24 +166,32 @@ public void voteForTopicByMember(final Long topicId, final Long memberId, final
Topic topic = findTopic(topicId);
final LocalDateTime votedAt = convertUnixTime(request.votedAt());

checkTopicVotable(topic, member, votedAt);

doVote(member, topic, votedAt, request.choiceOption());
voteForTopic(member, topic, votedAt, request.choiceOption());
}

private void doVote(final Member member, final Topic topic, final LocalDateTime votedAt, ChoiceOption choiceOption) {
private void voteForTopic(final Member member, final Topic topic, final LocalDateTime votedAt, ChoiceOption choiceOption) {
checkMemberVotableForTopic(member, topic, votedAt);

Vote vote = new Vote(choiceOption, votedAt);
vote.associate(member, topic);
voteRepository.save(vote);
}

private static void checkTopicVotable(final Topic topic, final Member member, final LocalDateTime votedAt) {
if (!topic.isBeforeDeadline(votedAt)) {
throw new UnableToVoteException(votedAt);
}
private void checkMemberVotableForTopic(final Member member, final Topic topic, final LocalDateTime votedAt) {
checkTopicVotable(topic, votedAt);
if (topic.isWrittenBy(member)) {
throw new VoteByAuthorException(topic.getId(), member.getId());
}
if (member.votedAlready(topic)) {
// 이미 투표했으면 또 투표 불가. 투표 다시하기 필요
throw new AlreadyVotedException(topic.getId(), member.getVotedOptionOfTopic(topic));
}
}

private void checkTopicVotable(final Topic topic, final LocalDateTime votedAt) {
if (!topic.isBeforeDeadline(votedAt)) {
throw new UnableToVoteException(votedAt);
}
final LocalDateTime now = LocalDateTime.now();
boolean votedAtFuture = votedAt.isAfter(now);
if (votedAtFuture) {
Expand All @@ -193,35 +201,31 @@ private static void checkTopicVotable(final Topic topic, final Member member, fi

@Transactional
public void modifyVoteForTopicByMember(final Long topicId, final Long memberId, final VoteModifyRequest request) {

final LocalDateTime modifiedAt = convertUnixTime(request.getModifiedAt());
final ChoiceOption modifiedOption = request.getModifiedOption();

Member member = findMember(memberId);
Topic topic = findTopic(topicId);
Vote vote = findVoteByMemberIdAndTopicId(memberId, topicId);

checkVoteModifiable(vote, modifiedOption, modifiedAt);

modifyVote(vote, modifiedOption, modifiedAt);
}

private void checkVoteModifiable(Vote vote, ChoiceOption modifiedOption, LocalDateTime modifiedAt) {

checkTopicVotable(vote.getTopic(), vote.getVoter(), modifiedAt);

if (vote.isVotedForOption(modifiedOption)) {
throw new DuplicateVoteException(vote.getTopic().getId(), modifiedOption);
}
}

private void modifyVote(Vote vote, ChoiceOption modifiedOption, LocalDateTime modifiedAt) {
checkVoteModifiable(vote, modifiedOption, modifiedAt);

deleteVotersComments(vote.getVoter(), vote.getTopic());

vote.changeOption(modifiedOption, modifiedAt);
}

private void checkVoteModifiable(Vote vote, ChoiceOption modifiedOption, LocalDateTime modifiedAt) {
checkTopicVotable(vote.getTopic(), modifiedAt);

boolean optionVotedAlready = vote.isVotedForOption(modifiedOption);
if (optionVotedAlready) {
throw new DuplicateVoteOptionException(vote.getTopic().getId(), modifiedOption);
}
}

private void deleteVotersComments(Member voter, Topic topic) {
int deleted = commentRepository.deleteAllByWriterIdAndTopicId(voter.getId(), topic.getId());
topic.commentRemoved(deleted);
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/life/offonoff/ab/exception/AbCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public enum AbCode {
ILLEGAL_AUTHOR,

// Vote
DUPLICATE_VOTE,
DUPLICATE_VOTE_OPTION,
ALREADY_VOTED,

// COMMENT
UNABLE_TO_VIEW_COMMENTS,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package life.offonoff.ab.exception;

import life.offonoff.ab.domain.topic.choice.ChoiceOption;

public class AlreadyVotedException extends DuplicateException {

private static final String MESSAGE = "이미 투표했습니다.";

private final Long topicId;
private final ChoiceOption votedOption;

public AlreadyVotedException(Long topicId, ChoiceOption votedOption) {
super(MESSAGE);
this.topicId = topicId;
this.votedOption = votedOption;
}

@Override
public String getHint() {
return "토픽[id=" + topicId + "]에 대해 투표 선택지[choiceOption=" + votedOption + "]로 이미 투표했습니다.";
}

@Override
public AbCode getAbCode() {
return AbCode.ALREADY_VOTED;
}

@Override
public Object getPayload() {
return votedOption;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

import life.offonoff.ab.domain.topic.choice.ChoiceOption;

public class DuplicateVoteException extends DuplicateException {
public class DuplicateVoteOptionException extends DuplicateException {

private static final String MESSAGE = "중복된 투표입니다.";
private static final String MESSAGE = "중복된 투표 선택지입니다.";

private final Long topicId;
private final ChoiceOption votedOption;

public DuplicateVoteException(Long topicId, ChoiceOption votedOption) {
public DuplicateVoteOptionException(Long topicId, ChoiceOption votedOption) {
super(MESSAGE);
this.topicId = topicId;
this.votedOption = votedOption;
Expand All @@ -22,6 +22,11 @@ public String getHint() {

@Override
public AbCode getAbCode() {
return AbCode.DUPLICATE_VOTE;
return AbCode.DUPLICATE_VOTE_OPTION;
}

@Override
public Object getPayload() {
return votedOption;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import life.offonoff.ab.domain.topic.TopicSide;
import life.offonoff.ab.domain.topic.choice.ChoiceOption;
import life.offonoff.ab.domain.vote.Vote;
import life.offonoff.ab.exception.AlreadyVotedException;
import life.offonoff.ab.exception.FutureTimeRequestException;
import life.offonoff.ab.exception.TopicReportDuplicateException;
import life.offonoff.ab.exception.VoteByAuthorException;
Expand Down Expand Up @@ -185,6 +186,22 @@ void voteForTopicByMember_votedAtFuture_throwException() {
.isInstanceOf(FutureTimeRequestException.class);
}

@Test
void voteForTopicByMember_duplicateVote_throwException() {
Member author = createMember();
Member voter = createMember();
TopicResponse response = createMembersTopic(author.getId());
VoteRequest request = new VoteRequest(
ChoiceOption.CHOICE_A, LocalDateTime.now().atZone(ZoneId.systemDefault()).toEpochSecond());
topicService.voteForTopicByMember(response.topicId(), voter.getId(), request);

ThrowingCallable code = () ->
topicService.voteForTopicByMember(response.topicId(), voter.getId(), request);

assertThatThrownBy(code)
.isInstanceOf(AlreadyVotedException.class);
}

private Member createMember() {
Member member = memberService.join(new SignUpRequest("email", "password", Provider.NONE));
member.registerPersonalInfo(new PersonalInfo("nickname", LocalDate.now(), Gender.MALE, "job"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import life.offonoff.ab.domain.topic.TopicStatus;
import life.offonoff.ab.domain.topic.choice.ChoiceOption;
import life.offonoff.ab.domain.vote.Vote;
import life.offonoff.ab.exception.DuplicateVoteException;
import life.offonoff.ab.exception.DuplicateVoteOptionException;
import life.offonoff.ab.exception.LengthInvalidException;
import life.offonoff.ab.repository.ChoiceRepository;
import life.offonoff.ab.repository.keyword.KeywordRepository;
Expand Down Expand Up @@ -345,8 +345,6 @@ void modify_vote() {
Comment comment1 = Comment.createVotersComment(vote, "content1");
Comment comment2 = Comment.createVotersComment(vote, "content2");

when(memberRepository.findByIdAndActiveTrue(anyLong())).thenReturn(Optional.of(voter));
when(topicRepository.findByIdAndActiveTrue(anyLong())).thenReturn(Optional.of(topic));
when(voteRepository.findByVoterIdAndTopicId(any(), any())).thenReturn(Optional.of(vote));
when(commentRepository.deleteAllByWriterIdAndTopicId(anyLong(), anyLong())).thenReturn(2);

Expand Down Expand Up @@ -386,8 +384,6 @@ void modify_vote_duplicate_exception() {
Vote vote = new Vote(ChoiceOption.CHOICE_A, LocalDateTime.now());
vote.associate(voter, topic);

when(memberRepository.findByIdAndActiveTrue(anyLong())).thenReturn(Optional.of(voter));
when(topicRepository.findByIdAndActiveTrue(anyLong())).thenReturn(Optional.of(topic));
when(voteRepository.findByVoterIdAndTopicId(any(), any())).thenReturn(Optional.of(vote));

// when
Expand All @@ -396,7 +392,7 @@ void modify_vote_duplicate_exception() {
topicId,
voter.getId(),
new VoteModifyRequest(vote.getSelectedOption(), getEpochSecond(deadline.minusHours(1))))
).isInstanceOf(DuplicateVoteException.class);
).isInstanceOf(DuplicateVoteOptionException.class);

}

Expand Down
35 changes: 28 additions & 7 deletions src/test/java/life/offonoff/ab/web/TopicControllerTest.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package life.offonoff.ab.web;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import life.offonoff.ab.application.service.CommentService;
import life.offonoff.ab.application.service.TopicService;
import life.offonoff.ab.application.service.TopicServiceTest.TopicTestDtoHelper;
import life.offonoff.ab.application.service.TopicServiceTest.TopicTestDtoHelper.TopicTestDtoHelperBuilder;
import life.offonoff.ab.application.service.request.*;
import life.offonoff.ab.application.service.request.TopicCreateRequest;
import life.offonoff.ab.application.service.request.TopicSearchRequest;
import life.offonoff.ab.application.service.request.VoteModifyRequest;
import life.offonoff.ab.application.service.request.VoteRequest;
import life.offonoff.ab.config.WebConfig;
import life.offonoff.ab.domain.comment.Comment;
import life.offonoff.ab.domain.keyword.Keyword;
Expand All @@ -22,19 +24,20 @@
import life.offonoff.ab.web.common.aspect.auth.AuthorizedArgumentResolver;
import life.offonoff.ab.web.response.CommentResponse;
import life.offonoff.ab.web.response.topic.TopicResponse;
import life.offonoff.ab.web.response.topic.content.TopicContentResponseFactory;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.data.domain.*;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand Down Expand Up @@ -242,6 +245,24 @@ void voteForTopic_votedAtFuture_throwException() throws Exception {
.andExpect(jsonPath("abCode").value("FUTURE_TIME_REQUEST"));
}

@Test
void voteForTopic_duplicateVote_throwException() throws Exception {
LocalDateTime now = LocalDateTime.now();
LocalDateTime votedAt = now.plusMinutes(30);

doThrow(new AlreadyVotedException(1L, ChoiceOption.CHOICE_B))
.when(topicService).voteForTopicByMember(any(), any(), any());

VoteRequest request = new VoteRequest(
ChoiceOption.CHOICE_A, votedAt.atZone(ZoneId.systemDefault()).toEpochSecond());
mvc.perform(post(TopicUri.VOTE, 1).with(csrf().asHeader())
.contentType(MediaType.APPLICATION_JSON)
.content(new ObjectMapper().registerModule(new JavaTimeModule()) // For serializing localdatetime
.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("abCode").value(AbCode.ALREADY_VOTED.name()));
}

@Test
void modifyVoteForTopic_not_duplicated_option() throws Exception {

Expand All @@ -267,7 +288,7 @@ void modifyVoteForTopic_exception_duplicated_option() throws Exception {
modifiedOption, getEpochSecond(LocalDateTime.now().plusMinutes(30))
);

doThrow(new DuplicateVoteException(topicId, modifiedOption))
doThrow(new DuplicateVoteOptionException(topicId, modifiedOption))
.when(topicService).modifyVoteForTopicByMember(any(), any(), any());

mvc.perform(patch(TopicUri.VOTE, topicId).with(csrf().asHeader())
Expand All @@ -276,7 +297,7 @@ modifiedOption, getEpochSecond(LocalDateTime.now().plusMinutes(30))
.writeValueAsString(request)))
.andExpectAll(
status().isBadRequest(),
jsonPath("$.abCode").value(AbCode.DUPLICATE_VOTE.name())
jsonPath("$.abCode").value(AbCode.DUPLICATE_VOTE_OPTION.name())
);
}

Expand Down

0 comments on commit e8c2ac7

Please sign in to comment.