Skip to content

Commit

Permalink
미션 목록 조회 구현 (issue #200) (#206)
Browse files Browse the repository at this point in the history
* feat: 미션 상세 조회 서비스 구현

* test: 미션 상세 조회 api 테스트

* feat: mission api swagger 적용
  • Loading branch information
alstn113 authored Aug 7, 2024
1 parent 4655b03 commit f678b8b
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 6 deletions.
26 changes: 24 additions & 2 deletions backend/src/main/java/develup/api/MissionApi.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
package develup.api;

import java.util.List;
import develup.api.auth.Auth;
import develup.api.common.ApiResponse;
import develup.application.auth.Accessor;
import develup.application.mission.MissionResponse;
import develup.application.mission.MissionService;
import develup.application.mission.MissionWithStartedResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Tag(name = "미션 API")
public class MissionApi {

private final MissionService missionService;
Expand All @@ -18,9 +25,24 @@ public MissionApi(MissionService missionService) {
}

@GetMapping("/missions")
@Operation(summary = "미션 목록 조회 API", description = "미션 목록을 조회합니다.")
public ResponseEntity<ApiResponse<List<MissionResponse>>> getMissions() {
List<MissionResponse> missions = missionService.getMissions();
List<MissionResponse> responses = missionService.getMissions();

return ResponseEntity.ok(new ApiResponse<>(missions));
return ResponseEntity.ok(new ApiResponse<>(responses));
}

@GetMapping("/missions/{missionId}")
@Operation(
summary = "미션 조회 API",
description = "미션을 조회합니다. 비로그인, 로그인 사용자 중 제출 여부에 따라 시작 상태 응답이 달라집니다."
)
public ResponseEntity<ApiResponse<MissionWithStartedResponse>> getMission(
@PathVariable Long missionId,
@Auth(required = false) Accessor accessor
) {
MissionWithStartedResponse response = missionService.getMission(accessor, missionId);

return ResponseEntity.ok(new ApiResponse<>(response));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ public enum ExceptionType {
TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "토큰이 존재하지 않습니다."),
TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다."),
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않는 토큰입니다."),

;
MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 미션입니다.");

private final HttpStatus status;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,42 @@
package develup.application.mission;

import java.util.List;
import develup.api.exception.DevelupException;
import develup.api.exception.ExceptionType;
import develup.application.auth.Accessor;
import develup.domain.mission.Mission;
import develup.domain.mission.MissionRepository;
import develup.domain.solution.SolutionRepository;
import develup.domain.solution.SolutionStatus;
import org.springframework.stereotype.Service;

@Service
public class MissionService {

private final MissionRepository missionRepository;
private final SolutionRepository solutionRepository;

public MissionService(MissionRepository missionRepository) {
public MissionService(MissionRepository missionRepository, SolutionRepository solutionRepository) {
this.missionRepository = missionRepository;
this.solutionRepository = solutionRepository;
}

public List<MissionResponse> getMissions() {
return missionRepository.findAll().stream()
.map(MissionResponse::from)
.toList();
}

public MissionWithStartedResponse getMission(Accessor accessor, Long missionId) {
Mission mission = missionRepository.findById(missionId)
.orElseThrow(() -> new DevelupException(ExceptionType.MISSION_NOT_FOUND));

if (accessor.isGuest()) {
return MissionWithStartedResponse.guest(mission);
}

boolean isStarted = solutionRepository
.existsByMember_IdAndMission_IdAndStatus(accessor.id(), missionId, SolutionStatus.IN_PROGRESS);
return MissionWithStartedResponse.of(mission, isStarted);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package develup.application.mission;

import develup.domain.mission.Mission;

public record MissionWithStartedResponse(
Long id,
String title,
String descriptionUrl,
String thumbnail,
String url,
boolean isStarted
) {

public static MissionWithStartedResponse of(Mission mission, boolean isStarted) {
return new MissionWithStartedResponse(
mission.getId(),
mission.getTitle(),
mission.getDescriptionUrl(),
mission.getThumbnail(),
mission.getUrl(),
isStarted
);
}

public static MissionWithStartedResponse guest(Mission mission) {
return of(mission, false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package develup.domain.solution;

import org.springframework.data.jpa.repository.JpaRepository;

public interface SolutionRepository extends JpaRepository<Solution, Long> {

boolean existsByMember_IdAndMission_IdAndStatus(Long memberId, Long missionId, SolutionStatus status);
}
1 change: 1 addition & 0 deletions backend/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
spring:
profiles:
active: local
group:
local: local, common
dev: dev, common
28 changes: 28 additions & 0 deletions backend/src/test/java/develup/api/MissionApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
Expand All @@ -10,18 +12,25 @@
import java.util.List;
import develup.application.mission.MissionResponse;
import develup.application.mission.MissionService;
import develup.application.mission.MissionWithStartedResponse;
import develup.domain.mission.Mission;
import develup.domain.mission.MissionRepository;
import develup.support.IntegrationTestSupport;
import develup.support.data.MissionTestData;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.BDDMockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;

class MissionApiTest extends IntegrationTestSupport {

@MockBean
private MissionService missionService;

@Autowired
private MissionRepository missionRepository;

@Test
@DisplayName("미션 목록을 조회한다.")
void getMissions() throws Exception {
Expand All @@ -45,4 +54,23 @@ void getMissions() throws Exception {
.andExpect(jsonPath("$.data[1].descriptionUrl", equalTo("https://raw.githubusercontent.com/develup-mission/mission/main/README.md")))
.andExpect(jsonPath("$.data.length()", is(2)));
}

@Test
@DisplayName("미션을 조회한다.")
void getMission() throws Exception {
Mission mission = missionRepository.save(MissionTestData.defaultMission().build());
MissionWithStartedResponse response = MissionWithStartedResponse.of(mission, false);
BDDMockito.given(missionService.getMission(any(), anyLong()))
.willReturn(response);

mockMvc.perform(get("/missions/1"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.id", equalTo(1)))
.andExpect(jsonPath("$.data.title", equalTo("루터회관 흡연단속")))
.andExpect(jsonPath("$.data.thumbnail", equalTo("https://thumbnail.com/1.png")))
.andExpect(jsonPath("$.data.url", equalTo("https://github.com/develup/mission")))
.andExpect(jsonPath("$.data.descriptionUrl", equalTo("https://raw.githubusercontent.com/develup-mission/mission/main/README.md")))
.andExpect(jsonPath("$.data.isStarted", is(false)));
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
package develup.application.mission;

import static develup.domain.solution.SolutionStatus.IN_PROGRESS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.util.List;
import develup.api.exception.DevelupException;
import develup.application.auth.Accessor;
import develup.domain.member.Member;
import develup.domain.member.MemberRepository;
import develup.domain.mission.Mission;
import develup.domain.mission.MissionRepository;
import develup.domain.solution.SolutionRepository;
import develup.support.IntegrationTestSupport;
import develup.support.data.MemberTestData;
import develup.support.data.MissionTestData;
import develup.support.data.SolutionTestData;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -18,6 +28,12 @@ class MissionServiceTest extends IntegrationTestSupport {
@Autowired
private MissionRepository missionRepository;

@Autowired
private MemberRepository memberRepository;

@Autowired
private SolutionRepository solutionRepository;

@Test
@DisplayName("미션 목록을 조회한다.")
void getMissions() {
Expand All @@ -26,6 +42,52 @@ void getMissions() {

List<MissionResponse> responses = missionService.getMissions();

assertThat(responses.size()).isEqualTo(2);
assertThat(responses).hasSize(2);
}

@Test
@DisplayName("존재하지 않는 미션 식별자로 미션 조회 시 예외가 발생한다.")
void getMissionFailWhenInvalidMissionId() {
assertThatThrownBy(() -> missionService.getMission(Accessor.GUEST, -1L))
.isInstanceOf(DevelupException.class)
.hasMessage("존재하지 않는 미션입니다.");
}

@Test
@DisplayName("비로그인 사용자가 미션 조회 시 시작 상태는 false이다.")
void getMission_guest() {
Mission mission = missionRepository.save(MissionTestData.defaultMission().build());
MissionWithStartedResponse response = missionService.getMission(Accessor.GUEST, mission.getId());

assertThat(response.isStarted()).isFalse();
}

@Test
@DisplayName("미션을 시작하지 않은 로그인 사용자가 미션 조회 시 시작 상태는 false이다.")
void getMission_notStarted() {
Mission mission = missionRepository.save(MissionTestData.defaultMission().build());
Member member = memberRepository.save(MemberTestData.defaultMember().build());
Accessor accessor = new Accessor(member.getId());

MissionWithStartedResponse response = missionService.getMission(accessor, mission.getId());

assertThat(response.isStarted()).isFalse();
}

@Test
@DisplayName("미션을 시작한 로그인 사용자가 미션 조회 시 시작 상태는 true이다.")
void getMission_started() {
Member member = memberRepository.save(MemberTestData.defaultMember().build());
Mission mission = missionRepository.save(MissionTestData.defaultMission().build());
solutionRepository.save(SolutionTestData.defaultSolution()
.withMember(member)
.withMission(mission)
.withStatus(IN_PROGRESS)
.build());
Accessor accessor = new Accessor(member.getId());

MissionWithStartedResponse response = missionService.getMission(accessor, mission.getId());

assertThat(response.isStarted()).isTrue();
}
}

0 comments on commit f678b8b

Please sign in to comment.