diff --git a/src/main/java/com/opus/opus/docs/asciidoc/contest-category.adoc b/src/main/java/com/opus/opus/docs/asciidoc/contest-category.adoc new file mode 100644 index 00000000..27c0b671 --- /dev/null +++ b/src/main/java/com/opus/opus/docs/asciidoc/contest-category.adoc @@ -0,0 +1,89 @@ +ifndef::snippets[] +:snippets: ./build/generated-snippets +endif::[] + += CONTEST CATEGORY API 문서 +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 3 +:sectnums: + +== API 목록 + +link:./opus.html[API 목록으로 돌아가기] + +== `POST`: 대회 카테고리 생성 + +.HTTP Request Headers +include::{snippets}/create-contest-category/request-headers.adoc[] + +.HTTP Request +include::{snippets}/create-contest-category/http-request.adoc[] + +.HTTP Response +include::{snippets}/create-contest-category/http-response.adoc[] + +.Request Fields +include::{snippets}/create-contest-category/request-fields.adoc[] + +=== ⚠️ 실패 케이스 + +.❌ Case 1: 동일한 카테고리 이름 존재 + +[%collapsible] + +==== + +include::{snippets}/create-contest-category-fail/http-request.adoc[] + +include::{snippets}/create-contest-category-fail/http-response.adoc[] + +include::{snippets}/create-contest-category-fail/request-fields.adoc[] + +==== + +== `PATCH`: 대회 카테고리 수정 + +NOTE: 대회 카테고리 생성과 동일하게 이미 저장되어 있는 카테고리 이름은 저장 불가 + +.HTTP Request Headers +include::{snippets}/update-contest-category/request-headers.adoc[] + +.HTTP Request +include::{snippets}/update-contest-category/http-request.adoc[] + +.HTTP Response +include::{snippets}/update-contest-category/http-response.adoc[] + +.Request Fields +include::{snippets}/update-contest-category/request-fields.adoc[] + +.Path Parameters +include::{snippets}/update-contest-category/path-parameters.adoc[] + +== `DELETE`: 대회 카테고리 삭제 + +.HTTP Request Headers +include::{snippets}/delete-contest-category/request-headers.adoc[] + +.HTTP Request +include::{snippets}/delete-contest-category/http-request.adoc[] + +.HTTP Response +include::{snippets}/delete-contest-category/http-response.adoc[] + +.Path Parameters +include::{snippets}/delete-contest-category/path-parameters.adoc[] + +== `GET`: 대회 카테고리 목록 조회 + +.HTTP Request +include::{snippets}/get-all-contest-category/http-request.adoc[] + +.HTTP Response +include::{snippets}/get-all-contest-category/http-response.adoc[] + +.Response Body's Fields +include::{snippets}/get-all-contest-category/response-fields.adoc[] diff --git a/src/main/java/com/opus/opus/docs/asciidoc/contest-track.adoc b/src/main/java/com/opus/opus/docs/asciidoc/contest-track.adoc new file mode 100644 index 00000000..98cc9fe2 --- /dev/null +++ b/src/main/java/com/opus/opus/docs/asciidoc/contest-track.adoc @@ -0,0 +1,92 @@ +ifndef::snippets[] +:snippets: ./build/generated-snippets +endif::[] + += CONTEST TRACK API 문서 +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 3 +:sectnums: + +== API 목록 + +link:./opus.html[API 목록으로 돌아가기] + +== `POST`: 대회 분과 생성 + +.HTTP Request Headers +include::{snippets}/create-contest-track/request-headers.adoc[] + +.HTTP Request +include::{snippets}/create-contest-track/http-request.adoc[] + +.HTTP Response +include::{snippets}/create-contest-track/http-response.adoc[] + +.Request Fields +include::{snippets}/create-contest-track/request-fields.adoc[] + +.Path Parameters +include::{snippets}/create-contest-track/path-parameters.adoc[] + +=== ⚠️ 실패 케이스 + +.❌ Case 1: 동일한 분과 이름 존재 + +[%collapsible] + +==== + +include::{snippets}/create-contest-track-fail/http-request.adoc[] + +include::{snippets}/create-contest-track-fail/http-response.adoc[] + +include::{snippets}/create-contest-track-fail/request-fields.adoc[] + +==== + +== `PATCH`: 대회 분과 수정 + +NOTE: 대회 분과 생성과 동일하게 이미 저장되어 있는 분과 이름은 저장 불가 + +.HTTP Request Headers +include::{snippets}/update-contest-track/request-headers.adoc[] + +.HTTP Request +include::{snippets}/update-contest-track/http-request.adoc[] + +.HTTP Response +include::{snippets}/update-contest-track/http-response.adoc[] + +.Request Fields +include::{snippets}/update-contest-track/request-fields.adoc[] + +.Path Parameters +include::{snippets}/update-contest-track/path-parameters.adoc[] + +== `DELETE`: 대회 분과 삭제 + +.HTTP Request Headers +include::{snippets}/delete-contest-track/request-headers.adoc[] + +.HTTP Request +include::{snippets}/delete-contest-track/http-request.adoc[] + +.HTTP Response +include::{snippets}/delete-contest-track/http-response.adoc[] + +.Path Parameters +include::{snippets}/delete-contest-track/path-parameters.adoc[] + +== `GET`: 대회 분과 목록 조회 + +.HTTP Request +include::{snippets}/get-all-contest-track/http-request.adoc[] + +.HTTP Response +include::{snippets}/get-all-contest-track/http-response.adoc[] + +.Response Body's Fields +include::{snippets}/get-all-contest-track/response-fields.adoc[] diff --git a/src/main/java/com/opus/opus/docs/asciidoc/contest.adoc b/src/main/java/com/opus/opus/docs/asciidoc/contest.adoc index 131d446b..e1f2c1b3 100644 --- a/src/main/java/com/opus/opus/docs/asciidoc/contest.adoc +++ b/src/main/java/com/opus/opus/docs/asciidoc/contest.adoc @@ -193,3 +193,79 @@ include::{snippets}/delete-contest-banner/http-request.adoc[] .HTTP Response include::{snippets}/delete-contest-banner/http-response.adoc[] + +== 대회 관리 + +=== `POST`: 대회 생성 + +.HTTP Request Headers +include::{snippets}/create-contest/request-headers.adoc[] + +.HTTP Request +include::{snippets}/create-contest/http-request.adoc[] + +.HTTP Response +include::{snippets}/create-contest/http-response.adoc[] + +.Request Fields +include::{snippets}/create-contest/request-fields.adoc[] + +==== ⚠️ 실패 케이스 + +.❌ Case 1: 동일한 대회명 존재 + +[%collapsible] + +==== + +include::{snippets}/create-contest-fail/http-request.adoc[] + +include::{snippets}/create-contest-fail/http-response.adoc[] + +include::{snippets}/create-contest-fail/request-fields.adoc[] + +==== + +=== `PATCH`: 대회 수정 + +NOTE: 대회 생성과 동일하게 이미 저장되어 있는 대회 이름은 저장 불가 + +.HTTP Request Headers +include::{snippets}/update-contest/request-headers.adoc[] + +.HTTP Request +include::{snippets}/update-contest/http-request.adoc[] + +.HTTP Response +include::{snippets}/update-contest/http-response.adoc[] + +.Request Fields +include::{snippets}/update-contest/request-fields.adoc[] + +.Path Parameters +include::{snippets}/update-contest/path-parameters.adoc[] + +=== `DELETE`: 대회 삭제 + +.HTTP Request Headers +include::{snippets}/delete-contest/request-headers.adoc[] + +.HTTP Request +include::{snippets}/delete-contest/http-request.adoc[] + +.HTTP Response +include::{snippets}/delete-contest/http-response.adoc[] + +.Path Parameters +include::{snippets}/delete-contest/path-parameters.adoc[] + +=== `GET`: 대회 목록 조회 + +.HTTP Request +include::{snippets}/get-all-contest/http-request.adoc[] + +.HTTP Response +include::{snippets}/get-all-contest/http-response.adoc[] + +.Response Body's Fields +include::{snippets}/get-all-contest/response-fields.adoc[] diff --git a/src/main/java/com/opus/opus/docs/asciidoc/opus.adoc b/src/main/java/com/opus/opus/docs/asciidoc/opus.adoc index 438881d1..a0c929c6 100644 --- a/src/main/java/com/opus/opus/docs/asciidoc/opus.adoc +++ b/src/main/java/com/opus/opus/docs/asciidoc/opus.adoc @@ -25,3 +25,7 @@ link:./notice.html[공지 API] == 대회 관련 API link:./contest.html[대회 API] + +link:./contest-category.html[대회 카테고리 API] + +link:./contest-track.html[대회 분과 API] diff --git a/src/main/java/com/opus/opus/modules/contest/api/ContestCategoryController.java b/src/main/java/com/opus/opus/modules/contest/api/ContestCategoryController.java index 9d9f136f..af32f999 100644 --- a/src/main/java/com/opus/opus/modules/contest/api/ContestCategoryController.java +++ b/src/main/java/com/opus/opus/modules/contest/api/ContestCategoryController.java @@ -31,8 +31,7 @@ public class ContestCategoryController { @Secured("ROLE_관리자") @PostMapping - public ResponseEntity createContestCategory( - @Valid @RequestBody final ContestCategoryRequest request) { + public ResponseEntity createContestCategory(@Valid @RequestBody final ContestCategoryRequest request) { contestCategoryCommandService.createCategory(request); return ResponseEntity.status(HttpStatus.CREATED).build(); } diff --git a/src/main/java/com/opus/opus/modules/contest/api/ContestController.java b/src/main/java/com/opus/opus/modules/contest/api/ContestController.java index 90a66bec..e7380983 100644 --- a/src/main/java/com/opus/opus/modules/contest/api/ContestController.java +++ b/src/main/java/com/opus/opus/modules/contest/api/ContestController.java @@ -71,8 +71,8 @@ public ResponseEntity> getAllContests() { return ResponseEntity.ok(responses); } - @Secured("ROLE_관리자") @PostMapping + @Secured("ROLE_관리자") public ResponseEntity createContest(@Valid @RequestBody final ContestRequest request) { ContestResponse response = contestCommandService.createContest(request); return ResponseEntity.ok(response); diff --git a/src/main/java/com/opus/opus/modules/contest/application/ContestTrackQueryService.java b/src/main/java/com/opus/opus/modules/contest/application/ContestTrackQueryService.java index fb798449..3da3db90 100644 --- a/src/main/java/com/opus/opus/modules/contest/application/ContestTrackQueryService.java +++ b/src/main/java/com/opus/opus/modules/contest/application/ContestTrackQueryService.java @@ -16,7 +16,7 @@ public class ContestTrackQueryService { private final ContestTrackRepository contestTrackRepository; public List getAllContestTracks(final Long contestId) { - List contestTracks = contestTrackRepository.findAllByContestId(contestId); + final List contestTracks = contestTrackRepository.findAllByContestId(contestId); return contestTracks.stream() .map(ContestTrackResponse::from) .toList(); diff --git a/src/main/java/com/opus/opus/modules/contest/application/dto/request/ContestCategoryRequest.java b/src/main/java/com/opus/opus/modules/contest/application/dto/request/ContestCategoryRequest.java index e5538540..676a774c 100644 --- a/src/main/java/com/opus/opus/modules/contest/application/dto/request/ContestCategoryRequest.java +++ b/src/main/java/com/opus/opus/modules/contest/application/dto/request/ContestCategoryRequest.java @@ -3,6 +3,7 @@ import jakarta.validation.constraints.NotBlank; public record ContestCategoryRequest( + @NotBlank(message = "카테고리명을 입력해주세요.") String categoryName ) { diff --git a/src/main/java/com/opus/opus/modules/contest/application/dto/request/ContestRequest.java b/src/main/java/com/opus/opus/modules/contest/application/dto/request/ContestRequest.java index 85c2bb20..692b3f81 100644 --- a/src/main/java/com/opus/opus/modules/contest/application/dto/request/ContestRequest.java +++ b/src/main/java/com/opus/opus/modules/contest/application/dto/request/ContestRequest.java @@ -4,6 +4,7 @@ import jakarta.validation.constraints.NotNull; public record ContestRequest( + @NotBlank(message = "대회명은 비어 있을 수 없습니다.") String contestName, @NotNull(message = "카테고리ID는 비어 있을 수 없습니다.") diff --git a/src/main/java/com/opus/opus/modules/contest/application/dto/request/ContestTrackRequest.java b/src/main/java/com/opus/opus/modules/contest/application/dto/request/ContestTrackRequest.java index 3b03b5ba..a0cae4cc 100644 --- a/src/main/java/com/opus/opus/modules/contest/application/dto/request/ContestTrackRequest.java +++ b/src/main/java/com/opus/opus/modules/contest/application/dto/request/ContestTrackRequest.java @@ -3,6 +3,7 @@ import jakarta.validation.constraints.NotBlank; public record ContestTrackRequest( + @NotBlank(message = "분과명은 비어 있을 수 없습니다.") String trackName ) { diff --git a/src/main/java/com/opus/opus/modules/contest/application/dto/response/ContestCategoryResponse.java b/src/main/java/com/opus/opus/modules/contest/application/dto/response/ContestCategoryResponse.java index df64d227..e4d8affa 100644 --- a/src/main/java/com/opus/opus/modules/contest/application/dto/response/ContestCategoryResponse.java +++ b/src/main/java/com/opus/opus/modules/contest/application/dto/response/ContestCategoryResponse.java @@ -4,6 +4,7 @@ import java.time.LocalDateTime; public record ContestCategoryResponse( + Long categoryId, String categoryName, LocalDateTime updatedAt diff --git a/src/main/java/com/opus/opus/modules/contest/application/dto/response/ContestResponse.java b/src/main/java/com/opus/opus/modules/contest/application/dto/response/ContestResponse.java index c3d27e50..047281d9 100644 --- a/src/main/java/com/opus/opus/modules/contest/application/dto/response/ContestResponse.java +++ b/src/main/java/com/opus/opus/modules/contest/application/dto/response/ContestResponse.java @@ -4,6 +4,7 @@ import java.time.LocalDateTime; public record ContestResponse( + Long contestId, String contestName, Long categoryId, diff --git a/src/main/java/com/opus/opus/modules/contest/domain/dao/ContestCategoryRepository.java b/src/main/java/com/opus/opus/modules/contest/domain/dao/ContestCategoryRepository.java index c140f8e4..e3206e99 100644 --- a/src/main/java/com/opus/opus/modules/contest/domain/dao/ContestCategoryRepository.java +++ b/src/main/java/com/opus/opus/modules/contest/domain/dao/ContestCategoryRepository.java @@ -4,5 +4,6 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface ContestCategoryRepository extends JpaRepository { + boolean existsByCategoryName(final String categoryName); } diff --git a/src/test/java/com/opus/opus/restdocs/RestDocsTest.java b/src/test/java/com/opus/opus/restdocs/RestDocsTest.java index 540299fd..6a53ee6b 100644 --- a/src/test/java/com/opus/opus/restdocs/RestDocsTest.java +++ b/src/test/java/com/opus/opus/restdocs/RestDocsTest.java @@ -7,9 +7,15 @@ import com.opus.opus.global.security.JwtProvider; import com.opus.opus.global.security.annotation.MemberArgumentResolver; import com.opus.opus.helper.ApiTestHelper; +import com.opus.opus.modules.contest.api.ContestCategoryController; import com.opus.opus.modules.contest.api.ContestController; +import com.opus.opus.modules.contest.api.ContestTrackController; +import com.opus.opus.modules.contest.application.ContestCategoryCommandService; +import com.opus.opus.modules.contest.application.ContestCategoryQueryService; import com.opus.opus.modules.contest.application.ContestCommandService; import com.opus.opus.modules.contest.application.ContestQueryService; +import com.opus.opus.modules.contest.application.ContestTrackCommandService; +import com.opus.opus.modules.contest.application.ContestTrackQueryService; import com.opus.opus.modules.member.api.MemberController; import com.opus.opus.modules.member.application.MemberCommandService; import com.opus.opus.modules.member.application.MemberQueryService; @@ -46,6 +52,8 @@ TeamMemberController.class, ContestController.class, TeamCommentController.class, + ContestCategoryController.class, + ContestTrackController.class, }) @Import(RestDocsConfig.class) @ExtendWith(RestDocumentationExtension.class) @@ -85,6 +93,18 @@ public abstract class RestDocsTest extends ApiTestHelper { @MockitoBean protected ContestQueryService contestQueryService; + @MockitoBean + protected ContestCategoryCommandService contestCategoryCommandService; + + @MockitoBean + protected ContestCategoryQueryService contestCategoryQueryService; + + @MockitoBean + protected ContestTrackCommandService contestTrackCommandService; + + @MockitoBean + protected ContestTrackQueryService contestTrackQueryService; + // Setting @Autowired protected WebApplicationContext context; diff --git a/src/test/java/com/opus/opus/restdocs/docs/ContestApiDocsTest.java b/src/test/java/com/opus/opus/restdocs/docs/ContestApiDocsTest.java index 1d451c52..8dd1cd05 100644 --- a/src/test/java/com/opus/opus/restdocs/docs/ContestApiDocsTest.java +++ b/src/test/java/com/opus/opus/restdocs/docs/ContestApiDocsTest.java @@ -1,7 +1,9 @@ package com.opus.opus.restdocs.docs; import static com.opus.opus.modules.contest.exception.ContestExceptionType.CANNOT_CHANGE_VOTES_DURING_VOTING_PERIOD; +import static com.opus.opus.modules.contest.exception.ContestExceptionType.CONTEST_NAME_ALREADY_EXIST; import static com.opus.opus.modules.contest.exception.ContestExceptionType.NOT_FOUND_CONTEST; +import static java.time.LocalDateTime.now; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willThrow; @@ -14,6 +16,7 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.multipart; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; @@ -24,8 +27,10 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.opus.opus.modules.contest.application.dto.request.ContestRequest; import com.opus.opus.modules.contest.application.dto.request.ContestVotesLimitRequest; import com.opus.opus.modules.contest.application.dto.request.VoteUpdateRequest; +import com.opus.opus.modules.contest.application.dto.response.ContestResponse; import com.opus.opus.modules.contest.application.dto.response.ContestVotesLimitResponse; import com.opus.opus.modules.contest.application.dto.response.VotePeriodResponse; import com.opus.opus.modules.contest.exception.ContestException; @@ -34,6 +39,7 @@ import com.opus.opus.modules.team.application.dto.ImageResponse; import com.opus.opus.restdocs.RestDocsTest; import java.time.LocalDateTime; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -154,7 +160,8 @@ void setUp() { parameterWithName("contestId").description("대회의 고유 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description(String.format(authorizationHeaderDescription, "admin")) + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "admin")) ), requestFields( numberFieldWithPath("maxVotesLimit", "최대 투표 개수") @@ -182,7 +189,8 @@ void setUp() { parameterWithName("contestId").description("대회 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description(String.format(authorizationHeaderDescription, "admin")) + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "admin")) ), requestFields( dateTimeFieldWithPath("voteStartAt", "투표 시작일"), @@ -210,7 +218,8 @@ void setUp() { parameterWithName("contestId").description("존재하지 않는 대회 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description(String.format(authorizationHeaderDescription, "admin")) + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "admin")) ), requestFields( numberFieldWithPath("maxVotesLimit", "최대 투표 개수") @@ -237,7 +246,8 @@ void setUp() { parameterWithName("contestId").description("대회 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description(String.format(authorizationHeaderDescription, "admin")) + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "admin")) ), requestFields( numberFieldWithPath("maxVotesLimit", "최대 투표 개수") @@ -260,7 +270,8 @@ void setUp() { parameterWithName("contestId").description("대회의 고유 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description(String.format(authorizationHeaderDescription, "admin")) + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "admin")) ), responseFields( numberFieldWithPath("maxVotesLimit", "최대 투표 개수") @@ -283,7 +294,8 @@ void setUp() { parameterWithName("contestId").description("존재하지 않는 대회 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description(String.format(authorizationHeaderDescription, "admin")) + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "admin")) ) )); } @@ -333,7 +345,8 @@ void setUp() { parameterWithName("contestId").description("대회 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description(String.format(authorizationHeaderDescription, "admin")) + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "admin")) ), requestParts( partWithName("image").description("등록할 배너 이미지 (모든 이미지 형식 지원)") @@ -357,7 +370,129 @@ void setUp() { parameterWithName("contestId").description("대회 ID") ), requestHeaders( - headerWithName(HttpHeaders.AUTHORIZATION).description(String.format(authorizationHeaderDescription, "admin")) + headerWithName(HttpHeaders.AUTHORIZATION).description( + String.format(authorizationHeaderDescription, "admin")) + ) + )); + } + + @Test + @DisplayName("[성공] 유효한 요청이면 대회 생성은 성공한다.") + void 유효한_요청이면_대회_카테고리_생성은_성공한다() throws Exception { + final ContestRequest request = new ContestRequest("제6회 해커톤", 1L); + final ContestResponse response = new ContestResponse(1L, request.contestName(), request.categoryId(), "해커톤", + true, now()); + + when(contestCommandService.createContest(any())).thenReturn(response); + + mockMvc.perform(post("/contests") + .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andDo(document("create-contest", + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer {accessToken} (관리자)") + ), + requestFields( + stringFieldWithPath("contestName", "대회 이름"), + numberFieldWithPath("categoryId", "카테고리 ID") + ), + responseFields( + numberFieldWithPath("contestId", "대회 ID"), + stringFieldWithPath("contestName", "대회 이름"), + numberFieldWithPath("categoryId", "카테고리 ID"), + stringFieldWithPath("categoryName", "카테고리 이름"), + booleanFieldWithPath("isCurrent", "현재 진행 대회 여부"), + dateTimeFieldWithPath("updatedAt", "수정 일시") + ) + )); + } + + @Test + @DisplayName("[실패] 이미 대회 이름이 존재한다면 에러를 반환한다.") + void 이미_대회_이름이_존재한다면_에러를_반환한다() throws Exception { + final ContestRequest request = new ContestRequest("제6회 해커톤", 1L); + + willThrow(new ContestException(CONTEST_NAME_ALREADY_EXIST)).given(contestCommandService).createContest(any()); + + mockMvc.perform(post("/contests") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(request))) + .andExpect(status().isConflict()) + .andDo(document("create-contest-fail", + requestFields( + stringFieldWithPath("contestName", "이미 존재하는 대회 이름"), + numberFieldWithPath("categoryId", "카테고리 ID") + ) + )); + } + + @Test + @DisplayName("[성공] 유효한 요청이면 대회 수정은 성공한다.") + void 유효한_요청이면_대회_수정은_성공한다() throws Exception { + final ContestRequest request = new ContestRequest("제6회 해커톤", 1L); + + doNothing().when(contestCommandService).updateContest(any(), any()); + + mockMvc.perform(patch("/contests/{contestId}", 1) + .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNoContent()) + .andDo(document("update-contest", + pathParameters( + parameterWithName("contestId").description("대회 ID") + ), + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer {accessToken} (관리자)") + ), + requestFields( + stringFieldWithPath("contestName", "대회 이름"), + numberFieldWithPath("categoryId", "카테고리 ID") + ) + )); + } + + @Test + @DisplayName("[성공] 유효한 요청이면 대회 삭제는 성공한다.") + void 유효한_요청이면_대회_삭제는_성공한다() throws Exception { + doNothing().when(contestCommandService).deleteContest(any()); + + mockMvc.perform(delete("/contests/{contestId}", 1) + .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN)) + .andExpect(status().isNoContent()) + .andDo(document("delete-contest", + pathParameters( + parameterWithName("contestId").description("대회 ID") + ), + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer {accessToken} (관리자)") + ) + )); + } + + @Test + @DisplayName("[성공] 유효한 요청이면 대회 전체 조회는 성공한다.") + void 유효한_요청이면_대회_전체_조회는_성공한다() throws Exception { + final List responses = List.of( + new ContestResponse(1L, "제6회 해커톤", 1L, "해커톤", true, now()), + new ContestResponse(2L, "CSE 캡스톤", 2L, "캡스톤", false, now()) + ); + + when(contestQueryService.getAllContests()).thenReturn(responses); + + mockMvc.perform(get("/contests")) + .andExpect(status().isOk()) + .andDo(document("get-all-contest", + responseFields( + arrayFieldWithPath("[]", "대회 목록"), + numberFieldWithPath("[].contestId", "대회 ID"), + stringFieldWithPath("[].contestName", "대회 이름"), + numberFieldWithPath("[].categoryId", "카테고리 ID"), + stringFieldWithPath("[].categoryName", "카테고리 이름"), + booleanFieldWithPath("[].isCurrent", "현재 진행 대회 여부"), + dateTimeFieldWithPath("[].updatedAt", "수정 일시") ) )); } diff --git a/src/test/java/com/opus/opus/restdocs/docs/ContestCategoryApiDocsTest.java b/src/test/java/com/opus/opus/restdocs/docs/ContestCategoryApiDocsTest.java new file mode 100644 index 00000000..27afe905 --- /dev/null +++ b/src/test/java/com/opus/opus/restdocs/docs/ContestCategoryApiDocsTest.java @@ -0,0 +1,150 @@ +package com.opus.opus.restdocs.docs; + +import static com.opus.opus.modules.contest.exception.ContestCategoryExceptionType.CATEGORY_NAME_ALREADY_EXIST; +import static java.time.LocalDateTime.now; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.util.ReflectionTestUtils.setField; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.opus.opus.member.MemberFixture; +import com.opus.opus.modules.contest.application.dto.request.ContestCategoryRequest; +import com.opus.opus.modules.contest.application.dto.response.ContestCategoryResponse; +import com.opus.opus.modules.contest.exception.ContestCategoryException; +import com.opus.opus.modules.member.domain.Member; +import com.opus.opus.restdocs.RestDocsTest; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +public class ContestCategoryApiDocsTest extends RestDocsTest { + + private Member admin; + private static final String ADMIN_TOKEN = "Bearer admin.access.token"; + + private ContestCategoryRequest request; + + @BeforeEach + void setUp() { + this.admin = MemberFixture.createMember(); + setField(admin, "id", 1L); + + request = new ContestCategoryRequest("캡스톤"); + } + + @Test + @DisplayName("[성공] 유효한 요청이면 대회 카테고리 생성은 성공한다.") + void 유효한_요청이면_대회_카테고리_생성은_성공한다() throws Exception { + doNothing().when(contestCategoryCommandService).createCategory(any()); + + mockMvc.perform(post("/categories") + .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andDo(document("create-contest-category", + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer {accessToken} (관리자)") + ), + requestFields( + stringFieldWithPath("categoryName", "카테고리 이름") + ) + )); + } + + @Test + @DisplayName("[실패] 이미 카테고리 이름이 존재한다면 에러를 반환한다.") + void 이미_카테고리_이름이_존재한다면_에러를_반환한다() throws Exception { + willThrow(new ContestCategoryException(CATEGORY_NAME_ALREADY_EXIST)).given(contestCategoryCommandService) + .createCategory(any()); + + mockMvc.perform(post("/categories") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(request))) + .andExpect(status().isConflict()) + .andDo(document("create-contest-category-fail", + requestFields( + stringFieldWithPath("categoryName", "이미 존재하는 카테고리 이름") + ) + )); + } + + @Test + @DisplayName("[성공] 유효한 요청이면 대회 카테고리 수정은 성공한다.") + void 유효한_요청이면_대회_카테고리_수정은_성공한다() throws Exception { + doNothing().when(contestCategoryCommandService).updateCategory(any(), any()); + + mockMvc.perform(patch("/categories/{categoryId}", 1) + .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNoContent()) + .andDo(document("update-contest-category", + pathParameters( + parameterWithName("categoryId").description("카테고리 ID") + ), + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer {accessToken} (관리자)") + ), + requestFields( + stringFieldWithPath("categoryName", "카테고리 이름") + ) + )); + } + + @Test + @DisplayName("[성공] 유효한 요청이면 대회 카테고리 삭제는 성공한다.") + void 유효한_요청이면_대회_카테고리_삭제는_성공한다() throws Exception { + doNothing().when(contestCategoryCommandService).deleteCategory(any()); + + mockMvc.perform(delete("/categories/{categoryId}", 1) + .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN)) + .andExpect(status().isNoContent()) + .andDo(document("delete-contest-category", + pathParameters( + parameterWithName("categoryId").description("카테고리 ID") + ), + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer {accessToken} (관리자)") + ) + )); + } + + @Test + @DisplayName("[성공] 유효한 요청이면 대회 카테고리 전체 조회는 성공한다.") + void 유효한_요청이면_대회_카테고리_전체_조회는_성공한다() throws Exception { + final List responses = List.of( + new ContestCategoryResponse(1L, "해커톤", now()), + new ContestCategoryResponse(2L, "자유대회", now()) + ); + + when(contestCategoryQueryService.getAllContestCategories()).thenReturn(responses); + + mockMvc.perform(get("/categories")) + .andExpect(status().isOk()) + .andDo(document("get-all-contest-category", + responseFields( + arrayFieldWithPath("[]", "대회 카테고리 목록"), + numberFieldWithPath("[].categoryId", "카테고리 ID"), + stringFieldWithPath("[].categoryName", "카테고리 이름"), + dateTimeFieldWithPath("[].updatedAt", "수정 일시") + ) + )); + } +} diff --git a/src/test/java/com/opus/opus/restdocs/docs/ContestTrackApiDocsTest.java b/src/test/java/com/opus/opus/restdocs/docs/ContestTrackApiDocsTest.java new file mode 100644 index 00000000..6c535d3f --- /dev/null +++ b/src/test/java/com/opus/opus/restdocs/docs/ContestTrackApiDocsTest.java @@ -0,0 +1,161 @@ +package com.opus.opus.restdocs.docs; + +import static com.opus.opus.modules.contest.exception.ContestTrackExceptionType.TRACKNAME_DUPLICATED; +import static java.time.LocalDateTime.now; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.util.ReflectionTestUtils.setField; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.opus.opus.member.MemberFixture; +import com.opus.opus.modules.contest.application.dto.request.ContestTrackRequest; +import com.opus.opus.modules.contest.application.dto.response.ContestTrackResponse; +import com.opus.opus.modules.contest.exception.ContestTrackException; +import com.opus.opus.modules.member.domain.Member; +import com.opus.opus.restdocs.RestDocsTest; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +public class ContestTrackApiDocsTest extends RestDocsTest { + + private Member admin; + private static final String ADMIN_TOKEN = "Bearer admin.access.token"; + + private ContestTrackRequest request; + + @BeforeEach + void setUp() { + this.admin = MemberFixture.createMember(); + setField(admin, "id", 1L); + + request = new ContestTrackRequest("창업"); + } + + @Test + @DisplayName("[성공] 유효한 요청이면 대회 분과 생성은 성공한다.") + void 유효한_요청이면_대회_분과_생성은_성공한다() throws Exception { + doNothing().when(contestTrackCommandService).createTrack(any(), any()); + + mockMvc.perform(post("/contests/{contestId}/tracks", 1) + .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andDo(document("create-contest-track", + pathParameters( + parameterWithName("contestId").description("대회 ID") + ), + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer {accessToken} (관리자)") + ), + requestFields( + stringFieldWithPath("trackName", "분과 이름") + ) + )); + } + + @Test + @DisplayName("[실패] 이미 분과 이름이 존재한다면 에러를 반환한다.") + void 이미_분과_이름이_존재한다면_에러를_반환한다() throws Exception { + willThrow(new ContestTrackException(TRACKNAME_DUPLICATED)).given(contestTrackCommandService) + .createTrack(any(), any()); + + mockMvc.perform(post("/contests/{contestId}/tracks", 1) + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(request))) + .andExpect(status().isConflict()) + .andDo(document("create-contest-track-fail", + pathParameters( + parameterWithName("contestId").description("대회 ID") + ), + requestFields( + stringFieldWithPath("trackName", "이미 존재하는 분과 이름") + ) + )); + } + + @Test + @DisplayName("[성공] 유효한 요청이면 대회 분과 수정은 성공한다.") + void 유효한_요청이면_대회_분과_수정은_성공한다() throws Exception { + doNothing().when(contestTrackCommandService).updateTrack(any(), any(), any()); + + mockMvc.perform(patch("/contests/{contestId}/tracks/{trackId}", 1, 1) + .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNoContent()) + .andDo(document("update-contest-track", + pathParameters( + parameterWithName("contestId").description("대회 ID"), + parameterWithName("trackId").description("분과 ID") + ), + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer {accessToken} (관리자)") + ), + requestFields( + stringFieldWithPath("trackName", "분과 이름") + ) + )); + } + + @Test + @DisplayName("[성공] 유효한 요청이면 대회 분과 삭제는 성공한다.") + void 유효한_요청이면_대회_분과_삭제는_성공한다() throws Exception { + doNothing().when(contestTrackCommandService).deleteTrack(any(), any()); + + mockMvc.perform(delete("/contests/{contestId}/tracks/{trackId}", 1, 1) + .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN)) + .andExpect(status().isNoContent()) + .andDo(document("delete-contest-track", + pathParameters( + parameterWithName("contestId").description("대회 ID"), + parameterWithName("trackId").description("분과 ID") + ), + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer {accessToken} (관리자)") + ) + )); + } + + @Test + @DisplayName("[성공] 유효한 요청이면 대회 분과 전체 조회는 성공한다.") + void 유효한_요청이면_대회_분과_전체_조회는_성공한다() throws Exception { + final List responses = List.of( + new ContestTrackResponse(1L, "창업", now()), + new ContestTrackResponse(2L, "융합", now()) + ); + + when(contestTrackQueryService.getAllContestTracks(any())).thenReturn(responses); + + mockMvc.perform(get("/contests/{contestId}/tracks", 1)) + .andExpect(status().isOk()) + .andDo(document("get-all-contest-track", + pathParameters( + parameterWithName("contestId").description("대회 ID") + ), + responseFields( + arrayFieldWithPath("[]", "대회 분과 목록"), + numberFieldWithPath("[].trackId", "분과 ID"), + stringFieldWithPath("[].trackName", "분과 이름"), + dateTimeFieldWithPath("[].updatedAt", "수정 일시") + ) + )); + } +}