From 0d850cf91f740cd1d048b4d792deb88f303046e5 Mon Sep 17 00:00:00 2001 From: Jinwook Kim Date: Fri, 20 Oct 2023 04:53:38 +0900 Subject: [PATCH] =?UTF-8?q?[BE]=20API=20=EB=AC=B8=EC=84=9C=20=EA=B3=A0?= =?UTF-8?q?=EB=8F=84=ED=99=94=20(=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stack/controller/StackController.java | 24 -- .../bootme/stack/service/StackService.java | 35 --- .../auth/controller/AuthControllerTest.java | 74 ++++- .../CourseBookmarkControllerTest.java | 94 ++++++- .../PostBookmarkControllerTest.java | 79 +++++- .../controller/CommentControllerTest.java | 67 ++++- .../controller/CompanyControllerTest.java | 89 +++++- .../controller/CourseControllerTest.java | 265 +++++++++++++++++- .../image/controller/ImageControllerTest.java | 17 +- .../controller/MemberControllerTest.java | 79 +++++- .../NotificationControllerTest.java | 43 ++- .../post/controller/PostControllerTest.java | 169 ++++++++++- .../com/bootme/sse/SseControllerTest.java | 9 +- .../stack/controller/StackControllerTest.java | 97 ++----- .../bootme/util/fixture/MemberFixture.java | 2 +- .../com/bootme/util/fixture/VoteFixture.java | 6 +- .../vote/controller/VoteControllerTest.java | 18 +- .../controller/WebhookControllerTest.java | 21 +- 18 files changed, 993 insertions(+), 195 deletions(-) diff --git a/backend/src/main/java/com/bootme/stack/controller/StackController.java b/backend/src/main/java/com/bootme/stack/controller/StackController.java index 5a9f379e..e34ca5c6 100644 --- a/backend/src/main/java/com/bootme/stack/controller/StackController.java +++ b/backend/src/main/java/com/bootme/stack/controller/StackController.java @@ -28,28 +28,4 @@ public ResponseEntity addStacks(@RequestBody List requests) return ResponseEntity.ok().build(); } - @PostMapping("/language") - public ResponseEntity addLanguage(@RequestParam String name, @RequestParam String icon) { - stackService.addLanguage(name, icon); - return ResponseEntity.ok().build(); - } - - @DeleteMapping("/language") - public ResponseEntity removeLanguage(@RequestParam String name) { - stackService.removeLanguage(name); - return ResponseEntity.ok().build(); - } - - @PostMapping("/framework") - public ResponseEntity addFramework(@RequestParam String name, @RequestParam String icon) { - stackService.addFramework(name, icon); - return ResponseEntity.ok().build(); - } - - @DeleteMapping("/framework") - public ResponseEntity removeFramework(@RequestParam String name) { - stackService.removeFramework(name); - return ResponseEntity.ok().build(); - } - } diff --git a/backend/src/main/java/com/bootme/stack/service/StackService.java b/backend/src/main/java/com/bootme/stack/service/StackService.java index ca9f091d..716ba648 100644 --- a/backend/src/main/java/com/bootme/stack/service/StackService.java +++ b/backend/src/main/java/com/bootme/stack/service/StackService.java @@ -57,36 +57,6 @@ public void addStacks(List requests) { } } - @Transactional - public void addLanguage(String name, String icon) { - validateDuplicate(name); - Stack stack = new Stack(name, LANGUAGE, icon); - stackRepository.save(stack); - languages.add(name); - } - - @Transactional - public void removeLanguage(String name) { - Stack stack = getStackByNameAndType(name, LANGUAGE); - stackRepository.delete(stack); - languages.remove(name); - } - - @Transactional - public void addFramework(String name, String icon) { - validateDuplicate(name); - Stack stack = new Stack(name, FRAMEWORK, icon); - stackRepository.save(stack); - frameworks.add(name); - } - - @Transactional - public void removeFramework(String name) { - Stack stack = getStackByNameAndType(name, FRAMEWORK); - stackRepository.delete(stack); - frameworks.remove(name); - } - @Transactional(readOnly = true) public List findAllStacks() { return stackRepository.findAll().stream() @@ -100,11 +70,6 @@ public Stack getStackByName(String name) { .orElseThrow(() -> new ResourceNotFoundException(NOT_FOUND_STACK, name)); } - private Stack getStackByNameAndType(String name, String type) { - return stackRepository.findByNameAndType(name, type) - .orElseThrow(() -> new ResourceNotFoundException(NOT_FOUND_STACK, name)); - } - private void validateDuplicate(String name){ boolean isExist = stackRepository.existsByName(name); if(isExist){ diff --git a/backend/src/test/java/com/bootme/auth/controller/AuthControllerTest.java b/backend/src/test/java/com/bootme/auth/controller/AuthControllerTest.java index 10faddd9..fcd565a5 100644 --- a/backend/src/test/java/com/bootme/auth/controller/AuthControllerTest.java +++ b/backend/src/test/java/com/bootme/auth/controller/AuthControllerTest.java @@ -5,6 +5,7 @@ import com.bootme.common.exception.TokenParseException; import com.bootme.common.exception.AuthenticationException; import com.bootme.util.ControllerTest; +import com.epages.restdocs.apispec.ResourceSnippetParameters; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -15,6 +16,7 @@ import static com.bootme.common.exception.ErrorType.*; import static com.bootme.util.fixture.AuthFixture.*; +import static com.epages.restdocs.apispec.ResourceDocumentation.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willDoNothing; @@ -22,6 +24,7 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -53,7 +56,22 @@ void login_success() throws Exception { perform.andDo(print()) .andDo(document("auth/login/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("구글, 카카오 로그인") + .description("구글, 카카오 로그인 (OpenID Connect)") + .requestHeaders( + headerWithName("Authorization").description("ID Token") + ) + .responseFields( + fieldWithPath("memberId").description("멤버 ID"), + fieldWithPath("email").description("이메일"), + fieldWithPath("nickname").description("닉네임"), + fieldWithPath("profileImage").description("프로필 사진"), + fieldWithPath("job").description("직업") + ) + .build()) + )); } @Test @@ -215,7 +233,22 @@ void naverLogin_success() throws Exception { perform.andDo(print()) .andDo(document("auth/login/naver/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("네이버 로그인") + .description("네이버 로그인 (OAuth 2.0)") + .requestFields( + fieldWithPath("url").description("네이버 OAuth URL") + ) + .responseFields( + fieldWithPath("memberId").description("멤버 ID"), + fieldWithPath("email").description("이메일"), + fieldWithPath("nickname").description("닉네임"), + fieldWithPath("profileImage").description("프로필 사진"), + fieldWithPath("job").description("직") + ) + .build()) + )); } @Test @@ -231,7 +264,12 @@ void logout() throws Exception { perform.andDo(print()) .andDo(document("auth/logout/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("로그아웃") + .description("로그아웃") + .build()) + )); } @Test @@ -257,7 +295,35 @@ void getSecrets_success() throws Exception { perform.andDo(print()) .andDo(document("auth/secrets/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("시크릿 요청") + .description("AWS Secrets Manager 저장된 시크릿 값 요청. (소셜 로그인 위한 API Key 등)") + .requestHeaders( + headerWithName("Bootme_Secret").description("JWT 검증 목적"), + headerWithName("Origin").description("허용된 origin 인지 검증 목적") + ) + .responseFields( + fieldWithPath("apiUrl").description("bootme 서버 URL"), + fieldWithPath("googleClientId").description("googleClientId"), + fieldWithPath("googleIssuer").description("googleIssuer"), + fieldWithPath("googleAudience").description("googleAudience"), + fieldWithPath("naverClientId").description("naverClientId"), + fieldWithPath("naverClientSecret").description("naverClientSecret"), + fieldWithPath("naverIssuer").description("naverIssuer"), + fieldWithPath("naverAudience").description("naverAudience"), + fieldWithPath("naverSigningKey").description("naverSigningKey"), + fieldWithPath("kakaoRestApiKey").description("kakaoRestApiKey"), + fieldWithPath("kakaoClientSecret").description("kakaoClientSecret"), + fieldWithPath("kakaoIssuer").description("kakaoIssuer"), + fieldWithPath("kakaoAudience").description("kakaoAudience"), + fieldWithPath("kakaoJavascriptKey").description("kakaoJavascriptKey"), + fieldWithPath("bootmeIssuer").description("bootmeIssuer"), + fieldWithPath("bootmeAudience").description("bootmeAudience"), + fieldWithPath("bootmeSigningKey").description("bootmeSigningKey") + ) + .build()) + )); } } \ No newline at end of file diff --git a/backend/src/test/java/com/bootme/bookmark/controller/CourseBookmarkControllerTest.java b/backend/src/test/java/com/bootme/bookmark/controller/CourseBookmarkControllerTest.java index 2e90d7ff..d999d993 100644 --- a/backend/src/test/java/com/bootme/bookmark/controller/CourseBookmarkControllerTest.java +++ b/backend/src/test/java/com/bootme/bookmark/controller/CourseBookmarkControllerTest.java @@ -2,6 +2,7 @@ import com.bootme.course.dto.CourseResponse; import com.bootme.util.ControllerTest; +import com.epages.restdocs.apispec.ResourceSnippetParameters; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -14,11 +15,14 @@ import java.util.List; import static com.bootme.util.fixture.CourseFixture.*; +import static com.epages.restdocs.apispec.ResourceDocumentation.*; +import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.*; import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -43,7 +47,16 @@ void addCourseBookmark() throws Exception { perform.andDo(print()) .andDo(document("courseBookmarks/add/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("북마크 코스 추가") + .description("북마크 코스 추가") + .pathParameters( + parameterWithName("memberId").description("memberId"), + parameterWithName("courseId").description("북마크 추가할 코스 ID") + ) + .build()) + )); } @Test @@ -63,7 +76,16 @@ void deleteCourseBookmark() throws Exception { perform.andDo(print()) .andDo(document("courseBookmarks/delete/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("북마크 코스 삭제") + .description("북마크 코스 삭제") + .pathParameters( + parameterWithName("memberId").description("memberId"), + parameterWithName("courseId").description("북마크 삭제할 코스 ID") + ) + .build()) + )); } @Test @@ -75,10 +97,12 @@ void getBookmarkedCourses() throws Exception { courseResponses.add(getCourseResponse(2)); Page coursePage = new PageImpl<>(courseResponses); - given(courseBookmarkService.getBookmarkedCourses(anyLong(), any())).willReturn(coursePage); + given(courseBookmarkService.getBookmarkedCourses(any(), any())).willReturn(coursePage); //when ResultActions perform = mockMvc.perform(get("/bookmarks/{memberId}/courses", 1) + .param("page", "1") + .param("size", "10") .accept(MediaType.APPLICATION_JSON)); //then @@ -88,7 +112,69 @@ void getBookmarkedCourses() throws Exception { perform.andDo(print()) .andDo(document("courseBookmarks/findAll/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("북마크한 코스 전체 조회") + .description("해당 회원이 북마크한 코스 전체를 반환한다.") + .pathParameters( + parameterWithName("memberId").description("memberId") + ) + .queryParameters( + parameterWithName("page").description("요청할 페이지 번호 (1부터 시작)"), + parameterWithName("size").description("한 페이지에 표시할 항목 수") + ) + .responseFields( + fieldWithPath("content[].id").description("코스 ID"), + fieldWithPath("content[].title").description("코스 이름 (연도, 기수 포함)"), + fieldWithPath("content[].name").description("코스 이름"), + fieldWithPath("content[].generation").description("코스 기수"), + fieldWithPath("content[].url").description("코스 URL"), + fieldWithPath("content[].location").description("위치"), + fieldWithPath("content[].superCategories[]").description("슈퍼 카테고리"), + fieldWithPath("content[].subCategories[]").description("서브 카테고리"), + fieldWithPath("content[].languages[]").description("프로그래밍 언어"), + fieldWithPath("content[].frameworks[]").description("프레임워크"), + fieldWithPath("content[].cost").description("코스 가격"), + fieldWithPath("content[].period").description("수강 기간"), + fieldWithPath("content[].dates.registrationStartDate").description("등록 시작 날짜"), + fieldWithPath("content[].dates.registrationEndDate").description("등록 종료 날짜"), + fieldWithPath("content[].dates.courseStartDate").description("코스 시작 날짜"), + fieldWithPath("content[].dates.courseEndDate").description("코스 종료 날짜"), + fieldWithPath("content[].company.id").description("회사 ID"), + fieldWithPath("content[].company.name").description("회사 이름"), + fieldWithPath("content[].company.serviceName").description("회사 운영 서비스 이름"), + fieldWithPath("content[].company.url").description("회사 URL"), + fieldWithPath("content[].company.serviceUrl").description("회사 운영 서비스 URL"), + fieldWithPath("content[].company.logoUrl").description("회사 로고 URL"), + fieldWithPath("content[].company.courses[]").description("회사 운영 코스"), + fieldWithPath("content[].clicks").description("클릭수"), + fieldWithPath("content[].bookmarks").description("북마크 수"), + fieldWithPath("content[].createdAt").description("생성 시간"), + fieldWithPath("content[].modifiedAt").description("수정 시간"), + fieldWithPath("content[].bookmarked").description("해당 회원의 북마크 여부"), + fieldWithPath("content[].recommended").description("추천 코스 여부"), + fieldWithPath("content[].free").description("무료 여부"), + fieldWithPath("content[].kdt").description("KDT 여부"), + fieldWithPath("content[].online").description("온라인 여부"), + fieldWithPath("content[].tested").description("코딩 테스트 여부"), + fieldWithPath("content[].prerequisiteRequired").description("사전 요구 사항 필요 여부"), + fieldWithPath("content[].registerOpen").description("등록 접수중 여부"), + fieldWithPath("pageable").description("pageable"), + fieldWithPath("last").description("마지막 페이지"), + fieldWithPath("totalPages").description("총 페이지 수"), + fieldWithPath("totalElements").description("총 아이템 수"), + fieldWithPath("size").description("크기"), + fieldWithPath("number").description("번호"), + fieldWithPath("sort.empty").description("정렬 비어 있음"), + fieldWithPath("sort.sorted").description("정렬 됨"), + fieldWithPath("sort.unsorted").description("미정렬"), + fieldWithPath("first").description("첫 페이지"), + fieldWithPath("numberOfElements").description("아이템 수"), + fieldWithPath("empty").description("비어 있음") + ) + .build() + ) + )); } } diff --git a/backend/src/test/java/com/bootme/bookmark/controller/PostBookmarkControllerTest.java b/backend/src/test/java/com/bootme/bookmark/controller/PostBookmarkControllerTest.java index 194b9938..e1b18903 100644 --- a/backend/src/test/java/com/bootme/bookmark/controller/PostBookmarkControllerTest.java +++ b/backend/src/test/java/com/bootme/bookmark/controller/PostBookmarkControllerTest.java @@ -2,6 +2,7 @@ import com.bootme.util.ControllerTest; import com.bootme.post.dto.PostResponse; +import com.epages.restdocs.apispec.ResourceSnippetParameters; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -14,12 +15,14 @@ import java.util.List; import static com.bootme.util.fixture.PostFixture.getPostResponse; +import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -31,7 +34,7 @@ class PostBookmarkControllerTest extends ControllerTest { @DisplayName("addPostBookmark()는 정상 요청시 북마크를 추가하고 상태코드 201을 반환한다.") void addPostBookmark() throws Exception { //given - given(postBookmarkService.addPostBookmark(anyLong(), anyLong())).willReturn(1L); + given(postBookmarkService.addPostBookmark(any(), any())).willReturn(1L); //when ResultActions perform = mockMvc.perform(post("/bookmarks/{memberId}/posts/{postId}", 1, 1) @@ -44,7 +47,16 @@ void addPostBookmark() throws Exception { perform.andDo(print()) .andDo(document("postBookmarks/add-post/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("북마크 게시글 추가") + .description("북마크 게시글 추가") + .pathParameters( + parameterWithName("memberId").description("memberId"), + parameterWithName("postId").description("북마크 추가할 게시글 ID") + ) + .build()) + )); } @Test @@ -61,7 +73,16 @@ void deletePostBookmark() throws Exception { perform.andDo(print()) .andDo(document("postBookmarks/delete-post/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("북마크 게시글 삭제") + .description("북마크 게시글 삭제") + .pathParameters( + parameterWithName("memberId").description("memberId"), + parameterWithName("postId").description("북마크 삭제할 게시글 ID") + ) + .build()) + )); } @Test @@ -73,10 +94,12 @@ void getBookmarkedPosts() throws Exception { postResponses.add(getPostResponse(2)); Page postPage = new PageImpl<>(postResponses); - given(postBookmarkService.getBookmarkedPosts(anyLong(), any())).willReturn(postPage); + given(postBookmarkService.getBookmarkedPosts(any(), any())).willReturn(postPage); //when ResultActions perform = mockMvc.perform(get("/bookmarks/{memberId}/posts", 1) + .param("page", "1") + .param("size", "10") .accept(MediaType.APPLICATION_JSON)); //then @@ -86,7 +109,51 @@ void getBookmarkedPosts() throws Exception { perform.andDo(print()) .andDo(document("postBookmarks/find-all-posts/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("북마크한 코스 전체 조회") + .description("해당 회원이 북마크한 코스 전체를 반환한다.") + .pathParameters( + parameterWithName("memberId").description("memberId") + ) + .queryParameters( + parameterWithName("page").description("요청할 페이지 번호 (1부터 시작)"), + parameterWithName("size").description("한 페이지에 표시할 항목 수") + ) + .responseFields( + fieldWithPath("content[].id").description("게시글 ID"), + fieldWithPath("content[].writerId").description("작성자 ID"), + fieldWithPath("content[].writerNickname").description("작성자 닉네임"), + fieldWithPath("content[].writerProfileImage").description("작성자 프로필 이미지 URL"), + fieldWithPath("content[].topic").description("주제"), + fieldWithPath("content[].title").description("제목"), + fieldWithPath("content[].contentExcerpt").description("내용 일부"), + fieldWithPath("content[].likes").description("좋아요 수"), + fieldWithPath("content[].clicks").description("클릭 수"), + fieldWithPath("content[].bookmarks").description("북마크 수"), + fieldWithPath("content[].status").description("게시글 디스플레이 상태"), + fieldWithPath("content[].createdAt").description("생성 시간"), + fieldWithPath("content[].modifiedAt").description("수정 시간"), + fieldWithPath("content[].commentCount").description("댓글 수"), + fieldWithPath("content[].voted").description("투표 상태"), + fieldWithPath("content[].bookmarked").description("북마크 여부"), + fieldWithPath("content[].viewed").description("조회 여부"), + fieldWithPath("pageable").description("pageable"), + fieldWithPath("last").description("마지막 페이지"), + fieldWithPath("totalPages").description("총 페이지 수"), + fieldWithPath("totalElements").description("총 아이템 수"), + fieldWithPath("size").description("크기"), + fieldWithPath("number").description("번호"), + fieldWithPath("sort.empty").description("정렬 비어 있음"), + fieldWithPath("sort.sorted").description("정렬 됨"), + fieldWithPath("sort.unsorted").description("미정렬"), + fieldWithPath("first").description("첫 페이지"), + fieldWithPath("numberOfElements").description("아이템 수"), + fieldWithPath("empty").description("비어 있음") + ) + .build() + ) + )); } } diff --git a/backend/src/test/java/com/bootme/comment/controller/CommentControllerTest.java b/backend/src/test/java/com/bootme/comment/controller/CommentControllerTest.java index 6c8488f0..efa993a0 100644 --- a/backend/src/test/java/com/bootme/comment/controller/CommentControllerTest.java +++ b/backend/src/test/java/com/bootme/comment/controller/CommentControllerTest.java @@ -1,6 +1,7 @@ package com.bootme.comment.controller; import com.bootme.util.ControllerTest; +import com.epages.restdocs.apispec.ResourceSnippetParameters; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -12,8 +13,11 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.*; import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.ResourceDocumentation.*; +import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -40,7 +44,18 @@ void addComment() throws Exception { perform.andDo(print()) .andDo(document("comments/add/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("댓글 추가") + .description("게시글에 댓글 추가") + .pathParameters( + parameterWithName("id").description("댓글 추가할 게시글 ID") + ) + .requestFields( + fieldWithPath("parentId").description("부모 댓글 ID").optional(), + fieldWithPath("content").description("댓글 내용")) + .build()) + )); } @Test @@ -59,7 +74,32 @@ void findComment() throws Exception { perform.andDo(print()) .andDo(document("comments/find/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("댓글 조회") + .description("댓글 조회") + .pathParameters( + parameterWithName("id").description("댓글 ID") + ) + .responseFields( + fieldWithPath("id").description("댓글 ID"), + fieldWithPath("postId").description("게시글 ID"), + fieldWithPath("writerId").description("작성자 ID"), + fieldWithPath("writerNickname").description("작성자 닉네임"), + fieldWithPath("writerProfileImage").description("작성자 프로필 이미지"), + fieldWithPath("parentId").description("부모 댓글 ID").optional(), + fieldWithPath("content").description("댓글 내용"), + fieldWithPath("groupNum").description("groupNum"), + fieldWithPath("levelNum").description("levelNum"), + fieldWithPath("orderNum").description("orderNum"), + fieldWithPath("likes").description("좋아요 수"), + fieldWithPath("voted").description("투표 상태"), + fieldWithPath("status").description("댓글 디스플레이 상태"), + fieldWithPath("createdAt").description("생성 시간"), + fieldWithPath("modifiedAt").description("수정 시간") + ) + .build()) + )); } @Test @@ -81,7 +121,18 @@ void modifyComment() throws Exception { perform.andDo(print()) .andDo(document("comments/modify/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("댓글 수정") + .description("댓글 수정") + .pathParameters( + parameterWithName("id").description("댓글 ID") + ) + .requestFields( + fieldWithPath("parentId").description("부모 댓글 ID").optional(), + fieldWithPath("content").description("댓글 내용")) + .build()) + )); } @Test @@ -101,7 +152,15 @@ void deleteComment() throws Exception { perform.andDo(print()) .andDo(document("comments/delete/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("댓글 삭제") + .description("댓글 삭제") + .pathParameters( + parameterWithName("id").description("댓글 ID") + ) + .build()) + )); } } diff --git a/backend/src/test/java/com/bootme/course/controller/CompanyControllerTest.java b/backend/src/test/java/com/bootme/course/controller/CompanyControllerTest.java index d2cab9c6..f65825a2 100644 --- a/backend/src/test/java/com/bootme/course/controller/CompanyControllerTest.java +++ b/backend/src/test/java/com/bootme/course/controller/CompanyControllerTest.java @@ -3,6 +3,7 @@ import com.bootme.common.exception.ResourceNotFoundException; import com.bootme.course.dto.CompanyResponse; import com.bootme.util.ControllerTest; +import com.epages.restdocs.apispec.ResourceSnippetParameters; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -15,6 +16,8 @@ import static com.bootme.common.exception.ErrorType.NOT_FOUND_COMPANY; import static com.bootme.util.fixture.CourseFixture.getCompanyRequest; import static com.bootme.util.fixture.CourseFixture.getCompanyResponse; +import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.*; import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; @@ -22,6 +25,7 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -50,7 +54,28 @@ void addCompany_success() throws Exception { perform.andDo(print()) .andDo(document("companies/add/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("회사 추가") + .description("회사 추가") + .requestFields( + fieldWithPath("name").description("회사 이름"), + fieldWithPath("serviceName").description("회사 운영 서비스 이름"), + fieldWithPath("url").description("회사 URL"), + fieldWithPath("serviceUrl").description("회사 운영 서비스 URL"), + fieldWithPath("logoUrl").description("회사 로고 URL") + ) + .responseFields( + fieldWithPath("id").description("회사 ID"), + fieldWithPath("name").description("회사 이름"), + fieldWithPath("serviceName").description("회사 운영 서비스 이름"), + fieldWithPath("url").description("회사 URL"), + fieldWithPath("serviceUrl").description("회사 운영 서비스 URL"), + fieldWithPath("logoUrl").description("회사 로고 URL"), + fieldWithPath("courses").description("회사 운영 코스") + ) + .build()) + )); } @Test @@ -70,7 +95,24 @@ void findCompany() throws Exception { perform.andDo(print()) .andDo(document("companies/find/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("회사 조회") + .description("회사 조회") + .pathParameters( + parameterWithName("id").description("회사 ID") + ) + .responseFields( + fieldWithPath("id").description("회사 ID"), + fieldWithPath("name").description("회사 이름"), + fieldWithPath("serviceName").description("회사 운영 서비스 이름"), + fieldWithPath("url").description("회사 URL"), + fieldWithPath("serviceUrl").description("회사 운영 서비스 URL"), + fieldWithPath("logoUrl").description("회사 로고 URL"), + fieldWithPath("courses").description("회사 운영 코스") + ) + .build()) + )); } @Test @@ -117,7 +159,21 @@ void findCompanies() throws Exception { perform.andDo(print()) .andDo(document("companies/findAll/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("회사 전체 조회") + .description("회사 전체 조회") + .responseFields( + fieldWithPath("[].id").description("회사 ID"), + fieldWithPath("[].name").description("회사 이름"), + fieldWithPath("[].serviceName").description("회사 운영 서비스 이름"), + fieldWithPath("[].url").description("회사 URL"), + fieldWithPath("[].serviceUrl").description("회사 운영 서비스 URL"), + fieldWithPath("[].logoUrl").description("회사 로고 URL"), + fieldWithPath("[].courses[]").description("회사 운영 코스") + ) + .build()) + )); } @Test @@ -139,7 +195,22 @@ void modifyCompany() throws Exception { perform.andDo(print()) .andDo(document("companies/modify/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("회사 수정") + .description("회사 수정") + .pathParameters( + parameterWithName("id").description("회사 ID") + ) + .requestFields( + fieldWithPath("name").description("회사 이름"), + fieldWithPath("serviceName").description("회사 운영 서비스 이름"), + fieldWithPath("url").description("회사 URL"), + fieldWithPath("serviceUrl").description("회사 운영 서비스 URL"), + fieldWithPath("logoUrl").description("회사 로고 URL") + ) + .build()) + )); } @Test @@ -185,7 +256,15 @@ void deleteCompany() throws Exception { perform.andDo(print()) .andDo(document("companies/delete/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("회사 삭제") + .description("회사 삭제") + .pathParameters( + parameterWithName("id").description("회사 ID") + ) + .build()) + )); } @Test diff --git a/backend/src/test/java/com/bootme/course/controller/CourseControllerTest.java b/backend/src/test/java/com/bootme/course/controller/CourseControllerTest.java index 30fbc5b2..0a557fc1 100644 --- a/backend/src/test/java/com/bootme/course/controller/CourseControllerTest.java +++ b/backend/src/test/java/com/bootme/course/controller/CourseControllerTest.java @@ -3,6 +3,7 @@ import com.bootme.common.exception.ResourceNotFoundException; import com.bootme.course.dto.CourseResponse; import com.bootme.util.ControllerTest; +import com.epages.restdocs.apispec.ResourceSnippetParameters; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -11,12 +12,14 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.ResultActions; -import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import static com.bootme.common.exception.ErrorType.NOT_FOUND_COMPANY; import static com.bootme.common.exception.ErrorType.NOT_FOUND_COURSE; import static com.bootme.util.fixture.CourseFixture.*; +import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.*; import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; @@ -24,6 +27,7 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -38,7 +42,7 @@ void addCourse() throws Exception { //given String content = objectMapper.writeValueAsString(getCourseRequest(1)); given(courseService.addCourse(any())).willReturn(1L); - given(courseService.findById(anyLong(), anyLong())).willReturn(getCourseDetailResponse(1)); + given(courseService.findById(any(), any())).willReturn(getCourseDetailResponse(1)); //when ResultActions perform = mockMvc.perform(post("/courses") @@ -52,7 +56,74 @@ void addCourse() throws Exception { perform.andDo(print()) .andDo(document("courses/add/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("코스 추가") + .description("코스 추가") + .requestFields( + fieldWithPath("title").description("코스 이름 (연도, 기수 포함)"), + fieldWithPath("name").description("코스 이름"), + fieldWithPath("generation").description("코스 기수"), + fieldWithPath("url").description("코스 URL"), + fieldWithPath("companyName").description("코스 운영 회사 이"), + fieldWithPath("location").description("위치"), + fieldWithPath("superCategories[]").description("슈퍼 카테고리"), + fieldWithPath("subCategories[]").description("서브 카테고리"), + fieldWithPath("languages[]").description("프로그래밍 언어"), + fieldWithPath("frameworks[]").description("프레임워크"), + fieldWithPath("cost").description("코스 가격"), + fieldWithPath("period").description("수강 기간"), + fieldWithPath("dates.registrationStartDate").description("등록 시작 날짜"), + fieldWithPath("dates.registrationEndDate").description("등록 종료 날짜"), + fieldWithPath("dates.courseStartDate").description("코스 시작 날짜"), + fieldWithPath("dates.courseEndDate").description("코스 종료 날짜"), + fieldWithPath("recommended").description("추천 코스 여부"), + fieldWithPath("free").description("무료 여부"), + fieldWithPath("kdt").description("KDT 여부"), + fieldWithPath("online").description("온라인 여부"), + fieldWithPath("tested").description("코딩 테스트 여부"), + fieldWithPath("prerequisiteRequired").description("사전 요구 사항 필요 여부") + ) + .responseFields( + fieldWithPath("id").description("코스 ID"), + fieldWithPath("title").description("코스 이름 (연도, 기수 포함)"), + fieldWithPath("name").description("코스 이름"), + fieldWithPath("generation").description("코스 기수"), + fieldWithPath("url").description("코스 URL"), + fieldWithPath("location").description("위치"), + fieldWithPath("superCategories[]").description("슈퍼 카테고리"), + fieldWithPath("subCategories[]").description("서브 카테고리"), + fieldWithPath("languages[]").description("프로그래밍 언어"), + fieldWithPath("frameworks[]").description("프레임워크"), + fieldWithPath("cost").description("코스 가격"), + fieldWithPath("period").description("수강 기간"), + fieldWithPath("dates.registrationStartDate").description("등록 시작 날짜"), + fieldWithPath("dates.registrationEndDate").description("등록 종료 날짜"), + fieldWithPath("dates.courseStartDate").description("코스 시작 날짜"), + fieldWithPath("dates.courseEndDate").description("코스 종료 날짜"), + fieldWithPath("detail").description("코스 상세 내용"), + fieldWithPath("company.id").description("회사 ID"), + fieldWithPath("company.name").description("회사 이름"), + fieldWithPath("company.serviceName").description("회사 운영 서비스 이름"), + fieldWithPath("company.url").description("회사 URL"), + fieldWithPath("company.serviceUrl").description("회사 운영 서비스 URL"), + fieldWithPath("company.logoUrl").description("회사 로고 URL"), + fieldWithPath("company.courses[]").description("회사 운영 코스"), + fieldWithPath("recommended").description("추천 코스 여부"), + fieldWithPath("free").description("무료 여부"), + fieldWithPath("kdt").description("KDT 여부"), + fieldWithPath("online").description("온라인 여부"), + fieldWithPath("tested").description("코딩 테스트 여부"), + fieldWithPath("prerequisiteRequired").description("사전 요구 사항 필요 여부"), + fieldWithPath("registerOpen").description("등록 접수중 여부"), + fieldWithPath("clicks").description("클릭수"), + fieldWithPath("bookmarks").description("북마크 수"), + fieldWithPath("createdAt").description("생성 시간"), + fieldWithPath("modifiedAt").description("수정 시간"), + fieldWithPath("bookmarked").description("북마크 여부") + ) + .build()) + )); } @Test @@ -85,7 +156,7 @@ void addCourse_NOT_FOUND_COMPANY() throws Exception { @DisplayName("findCourse()는 정상 요청시 상태코드 200을 반환한다.") void findCourse() throws Exception { //given - given(courseService.findById(anyLong(), anyLong())).willReturn(getCourseDetailResponse(1)); + given(courseService.findById(any(), any())).willReturn(getCourseDetailResponse(1)); //when ResultActions perform = mockMvc.perform(get("/courses/{id}", 1) @@ -98,7 +169,53 @@ void findCourse() throws Exception { perform.andDo(print()) .andDo(document("courses/find/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("코스 조회") + .description("코스 조회") + .pathParameters( + parameterWithName("id").description("코스 Id") + ) + .responseFields( + fieldWithPath("id").description("코스 ID"), + fieldWithPath("title").description("코스 이름 (연도, 기수 포함)"), + fieldWithPath("name").description("코스 이름"), + fieldWithPath("generation").description("코스 기수"), + fieldWithPath("url").description("코스 URL"), + fieldWithPath("location").description("위치"), + fieldWithPath("superCategories[]").description("슈퍼 카테고리"), + fieldWithPath("subCategories[]").description("서브 카테고리"), + fieldWithPath("languages[]").description("프로그래밍 언어"), + fieldWithPath("frameworks[]").description("프레임워크"), + fieldWithPath("cost").description("코스 가격"), + fieldWithPath("period").description("수강 기간"), + fieldWithPath("dates.registrationStartDate").description("등록 시작 날짜"), + fieldWithPath("dates.registrationEndDate").description("등록 종료 날짜"), + fieldWithPath("dates.courseStartDate").description("코스 시작 날짜"), + fieldWithPath("dates.courseEndDate").description("코스 종료 날짜"), + fieldWithPath("detail").description("코스 상세 내용"), + fieldWithPath("company.id").description("회사 ID"), + fieldWithPath("company.name").description("회사 이름"), + fieldWithPath("company.serviceName").description("회사 운영 서비스 이름"), + fieldWithPath("company.url").description("회사 URL"), + fieldWithPath("company.serviceUrl").description("회사 운영 서비스 URL"), + fieldWithPath("company.logoUrl").description("회사 로고 URL"), + fieldWithPath("company.courses[]").description("회사 운영 코스"), + fieldWithPath("recommended").description("추천 코스 여부"), + fieldWithPath("free").description("무료 여부"), + fieldWithPath("kdt").description("KDT 여부"), + fieldWithPath("online").description("온라인 여부"), + fieldWithPath("tested").description("코딩 테스트 여부"), + fieldWithPath("prerequisiteRequired").description("사전 요구 사항 필요 여부"), + fieldWithPath("registerOpen").description("등록 접수중 여부"), + fieldWithPath("clicks").description("클릭수"), + fieldWithPath("bookmarks").description("북마크 수"), + fieldWithPath("createdAt").description("생성 시간"), + fieldWithPath("modifiedAt").description("수정 시간"), + fieldWithPath("bookmarked").description("북마크 여부") + ) + .build()) + )); } @Test @@ -129,13 +246,14 @@ void findCourse_NOT_FOUND_COURSE() throws Exception { @DisplayName("findCourses()는 정상 요청시 상태코드 200을 반환한다.") void findCourses() throws Exception { //given - List courseResponses = new ArrayList<>(); - courseResponses.add(getCourseResponse(1)); - courseResponses.add(getCourseResponse(2)); - courseResponses.add(getCourseResponse(3)); + List courseResponses = Arrays.asList( + getCourseResponse(1), + getCourseResponse(2), + getCourseResponse(3) + ); Page coursePage = new PageImpl<>(courseResponses); - given(courseService.findAll(anyLong(), anyInt(), anyInt(), anyString(), any())).willReturn(coursePage); + given(courseService.findAll(any(), anyInt(), anyInt(), any(), any())).willReturn(coursePage); //when ResultActions perform = mockMvc.perform(get("/courses") @@ -148,7 +266,75 @@ void findCourses() throws Exception { perform.andDo(print()) .andDo(document("courses/findAll/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("코스 전체 조회") + .description("코스 전체 조회") + .queryParameters( + parameterWithName("page").description("요청할 페이지 번호 (1부터 시작)").optional(), + parameterWithName("size").description("한 페이지에 표시할 항목 수").optional(), + parameterWithName("sort").description("정렬 기준").optional(), + parameterWithName("superCategories").description("슈퍼 카테고리").optional(), + parameterWithName("subCategories").description("서브 카테고리").optional(), + parameterWithName("languages").description("프로그래밍 언어").optional(), + parameterWithName("frameworks").description("프레임워크").optional(), + parameterWithName("costInput").description("수강 비용 필터 입력값").optional(), + parameterWithName("periodInput").description("수강 기간 필터 입력값").optional(), + parameterWithName("isFree").description("무료 여부").optional(), + parameterWithName("isKdt").description("KDT 여부").optional(), + parameterWithName("isTested").description("코딩 테스트 여부").optional() + ) + .responseFields( + fieldWithPath("content[].id").description("코스 ID"), + fieldWithPath("content[].title").description("코스 이름 (연도, 기수 포함)"), + fieldWithPath("content[].name").description("코스 이름"), + fieldWithPath("content[].generation").description("코스 기수"), + fieldWithPath("content[].url").description("코스 URL"), + fieldWithPath("content[].location").description("위치"), + fieldWithPath("content[].superCategories[]").description("슈퍼 카테고리"), + fieldWithPath("content[].subCategories[]").description("서브 카테고리"), + fieldWithPath("content[].languages[]").description("프로그래밍 언어"), + fieldWithPath("content[].frameworks[]").description("프레임워크"), + fieldWithPath("content[].cost").description("코스 가격"), + fieldWithPath("content[].period").description("수강 기간"), + fieldWithPath("content[].dates.registrationStartDate").description("등록 시작 날짜"), + fieldWithPath("content[].dates.registrationEndDate").description("등록 종료 날짜"), + fieldWithPath("content[].dates.courseStartDate").description("코스 시작 날짜"), + fieldWithPath("content[].dates.courseEndDate").description("코스 종료 날짜"), + fieldWithPath("content[].company.id").description("회사 ID"), + fieldWithPath("content[].company.name").description("회사 이름"), + fieldWithPath("content[].company.serviceName").description("회사 운영 서비스 이름"), + fieldWithPath("content[].company.url").description("회사 URL"), + fieldWithPath("content[].company.serviceUrl").description("회사 운영 서비스 URL"), + fieldWithPath("content[].company.logoUrl").description("회사 로고 URL"), + fieldWithPath("content[].company.courses[]").description("회사 운영 코스"), + fieldWithPath("content[].clicks").description("클릭수"), + fieldWithPath("content[].bookmarks").description("북마크 수"), + fieldWithPath("content[].createdAt").description("생성 시간"), + fieldWithPath("content[].modifiedAt").description("수정 시간"), + fieldWithPath("content[].bookmarked").description("해당 회원의 북마크 여부"), + fieldWithPath("content[].recommended").description("추천 코스 여부"), + fieldWithPath("content[].free").description("무료 여부"), + fieldWithPath("content[].kdt").description("KDT 여부"), + fieldWithPath("content[].online").description("온라인 여부"), + fieldWithPath("content[].tested").description("코딩 테스트 여부"), + fieldWithPath("content[].prerequisiteRequired").description("사전 요구 사항 필요 여부"), + fieldWithPath("content[].registerOpen").description("등록 접수중 여부"), + fieldWithPath("pageable").description("pageable"), + fieldWithPath("last").description("마지막 페이지"), + fieldWithPath("totalPages").description("총 페이지 수"), + fieldWithPath("totalElements").description("총 아이템 수"), + fieldWithPath("size").description("크기"), + fieldWithPath("number").description("번호"), + fieldWithPath("sort.empty").description("정렬 비어 있음"), + fieldWithPath("sort.sorted").description("정렬 됨"), + fieldWithPath("sort.unsorted").description("미정렬"), + fieldWithPath("first").description("첫 페이지"), + fieldWithPath("numberOfElements").description("아이템 수"), + fieldWithPath("empty").description("비어 있음") + ) + .build()) + )); } @@ -171,7 +357,39 @@ void modifyCourse() throws Exception { perform.andDo(print()) .andDo(document("courses/modify/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("코스 수정") + .description("코스 수정") + .pathParameters( + parameterWithName("id").description("memberId") + ) + .requestFields( + fieldWithPath("title").description("코스 이름 (연도, 기수 포함)"), + fieldWithPath("name").description("코스 이름"), + fieldWithPath("generation").description("코스 기수"), + fieldWithPath("url").description("코스 URL"), + fieldWithPath("companyName").description("코스 운영 회사 이"), + fieldWithPath("location").description("위치"), + fieldWithPath("superCategories[]").description("슈퍼 카테고리"), + fieldWithPath("subCategories[]").description("서브 카테고리"), + fieldWithPath("languages[]").description("프로그래밍 언어"), + fieldWithPath("frameworks[]").description("프레임워크"), + fieldWithPath("cost").description("코스 가격"), + fieldWithPath("period").description("수강 기간"), + fieldWithPath("dates.registrationStartDate").description("등록 시작 날짜"), + fieldWithPath("dates.registrationEndDate").description("등록 종료 날짜"), + fieldWithPath("dates.courseStartDate").description("코스 시작 날짜"), + fieldWithPath("dates.courseEndDate").description("코스 종료 날짜"), + fieldWithPath("recommended").description("추천 코스 여부"), + fieldWithPath("free").description("무료 여부"), + fieldWithPath("kdt").description("KDT 여부"), + fieldWithPath("online").description("온라인 여부"), + fieldWithPath("tested").description("코딩 테스트 여부"), + fieldWithPath("prerequisiteRequired").description("사전 요구 사항 필요 여부") + ) + .build()) + )); } @Test @@ -219,7 +437,18 @@ void modifyCourseDetail() throws Exception { perform.andDo(print()) .andDo(document("courses/modify-detail/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("코스 상세 페이지 수정") + .description("코스 상세 페이지 수정") + .pathParameters( + parameterWithName("id").description("코스 ID") + ) + .requestFields( + fieldWithPath("detail").description("코스 상세 페이지 내용") + ) + .build()) + )); } @Test @@ -265,7 +494,15 @@ void deleteCourse() throws Exception { perform.andDo(print()) .andDo(document("courses/delete/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("코스 삭제") + .description("코스 삭제") + .pathParameters( + parameterWithName("id").description("코스 ID") + ) + .build()) + )); } @Test diff --git a/backend/src/test/java/com/bootme/image/controller/ImageControllerTest.java b/backend/src/test/java/com/bootme/image/controller/ImageControllerTest.java index bb7884b4..ff594c01 100644 --- a/backend/src/test/java/com/bootme/image/controller/ImageControllerTest.java +++ b/backend/src/test/java/com/bootme/image/controller/ImageControllerTest.java @@ -1,17 +1,21 @@ package com.bootme.image.controller; import com.bootme.util.ControllerTest; +import com.epages.restdocs.apispec.ResourceSnippetParameters; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.web.servlet.ResultActions; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -24,12 +28,13 @@ class ImageControllerTest extends ControllerTest { void upload() throws Exception { //given MockMultipartFile imageFile = new MockMultipartFile("image", "filename.jpg", "image/jpeg", "some-image".getBytes()); - given(imageService.upload(any(), any(), any())).willReturn("S3 업로드 된 이미지의 객체 URL"); + given(imageService.upload(any(), any(), any())).willReturn("https://bootme-images.s3.ap-northeast-2.amazonaws.com/etc/smile_face.png"); //when ResultActions perform = mockMvc.perform( RestDocumentationRequestBuilders.multipart("/images") .file(imageFile) + .contentType(MediaType.MULTIPART_FORM_DATA) .param("imageType", "PROFILE") ); @@ -40,7 +45,15 @@ void upload() throws Exception { perform.andDo(print()) .andDo(document("images/upload/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + responseBody(), + resource(ResourceSnippetParameters.builder() + .summary("이미지 업로드") + .description("텍스트 에디터에서 첨부된 이미지를 S3 버킷에 업로드 후 객체 URL을 반환한다." + + "

restdocs-api-spec 라이브러리에서 Mutlipart 파라미터 지원하지 않기 때문에 정상적으로 문서 작성하지 못함 " + + "
참고: https://github.com/ePages-de/restdocs-api-spec/issues/105") + .build()) + )); } } diff --git a/backend/src/test/java/com/bootme/member/controller/MemberControllerTest.java b/backend/src/test/java/com/bootme/member/controller/MemberControllerTest.java index 0650d3f3..7f998065 100644 --- a/backend/src/test/java/com/bootme/member/controller/MemberControllerTest.java +++ b/backend/src/test/java/com/bootme/member/controller/MemberControllerTest.java @@ -1,6 +1,7 @@ package com.bootme.member.controller; import com.bootme.util.ControllerTest; +import com.epages.restdocs.apispec.ResourceSnippetParameters; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -9,10 +10,12 @@ import static com.bootme.util.fixture.MemberFixture.*; import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; -import static org.mockito.ArgumentMatchers.anyLong; +import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; import static org.mockito.BDDMockito.*; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -24,7 +27,7 @@ class MemberControllerTest extends ControllerTest { @DisplayName("findMemberProfile()는 정상 요청시 회원 프로필을 반환하고 상태코드 200을 반환한다.") void findMemberProfile() throws Exception { //given - given(memberService.findMemberProfile(anyLong())).willReturn(getProfileResponse(1)); + given(memberService.findMemberProfile(any())).willReturn(getProfileResponse(1)); //when ResultActions perform = mockMvc.perform(get("/member/{memberId}/profile", 1) @@ -37,7 +40,24 @@ void findMemberProfile() throws Exception { perform.andDo(print()) .andDo(document("members/find-profile/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("회원 프로필 조회") + .description("커뮤니티 페이지에서 회원 프로필 사진을 클릭했을 때 해당 회원의 프로필 정보를 가져온다.") + .pathParameters( + parameterWithName("memberId").description("회원 ID") + ) + .responseFields( + fieldWithPath("profileImage").description("프로필 이미지"), + fieldWithPath("email").description("이메일"), + fieldWithPath("nickname").description("닉네임"), + fieldWithPath("job").description("직업"), + fieldWithPath("stacks[].name").description("기술 스택 이름"), + fieldWithPath("stacks[].type").description("기술 스택 유형"), + fieldWithPath("stacks[].icon").description("기술 스택 아이콘") + ) + .build()) + )); } @Test @@ -57,7 +77,19 @@ void findMyProfile() throws Exception { perform.andDo(print()) .andDo(document("members/find-my-profile/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("내 프로필 조회") + .description("프로필 관리 페이지 진입시 자신의 프로필 정보를 불러온다.") + .responseFields( + fieldWithPath("profileImage").description("프로필 이미지"), + fieldWithPath("email").description("이메일"), + fieldWithPath("nickname").description("닉네임"), + fieldWithPath("job").description("직업"), + fieldWithPath("stacks[]").description("기술 스택") + ) + .build()) + )); } @Test @@ -79,7 +111,21 @@ void modifyProfile() throws Exception { perform.andDo(print()) .andDo(document("members/modify-profile/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("프로필 수정") + .description("프로필 관리 페이지에서 정보 수정 후 저장하기 버튼 클릭시 해당 회원의 정보를 수정한다.") + .pathParameters( + parameterWithName("memberId").description("회원 ID") + ) + .requestFields( + fieldWithPath("email").description("이메일"), + fieldWithPath("nickname").description("닉네임"), + fieldWithPath("job").description("직업"), + fieldWithPath("stackNames[]").description("기술 스택") + ) + .build()) + )); } @Test @@ -101,7 +147,18 @@ void modifyProfileImage() throws Exception { perform.andDo(print()) .andDo(document("members/modify-profile-image/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("프로필 이미지 수정") + .description("프로필 관리 페이지에서 프로필 이미지 변경시 호출된다.") + .pathParameters( + parameterWithName("memberId").description("회원 ID") + ) + .requestFields( + fieldWithPath("profileImage").description("변경할 이미지 URL") + ) + .build()) + )); } @Test @@ -121,7 +178,15 @@ void checkNicknameDuplicate() throws Exception { perform.andDo(print()) .andDo(document("members/check-nickname/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("닉네임 중복 검사") + .description("프로필 관리 페이지에서 닉네임 중복 여부를 검사한다.") + .pathParameters( + parameterWithName("nickname").description("닉네임") + ) + .build()) + )); } } \ No newline at end of file diff --git a/backend/src/test/java/com/bootme/notification/controller/NotificationControllerTest.java b/backend/src/test/java/com/bootme/notification/controller/NotificationControllerTest.java index fa119a34..c9dc0585 100644 --- a/backend/src/test/java/com/bootme/notification/controller/NotificationControllerTest.java +++ b/backend/src/test/java/com/bootme/notification/controller/NotificationControllerTest.java @@ -2,6 +2,7 @@ import com.bootme.notification.dto.NotificationResponse; import com.bootme.util.ControllerTest; +import com.epages.restdocs.apispec.ResourceSnippetParameters; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -12,10 +13,14 @@ import java.util.List; import static com.bootme.util.fixture.NotificationFixture.getNotificationResponse; +import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -30,7 +35,7 @@ void findNotificationsByMemberId() throws Exception { NotificationResponse notificationResponse1 = getNotificationResponse(1); NotificationResponse notificationResponse2 = getNotificationResponse(2); NotificationResponse notificationResponse3 = getNotificationResponse(3); - given(notificationService.findNotificationsByMemberId(anyLong())).willReturn(List.of(notificationResponse1, notificationResponse2, notificationResponse3)); + given(notificationService.findNotificationsByMemberId(any())).willReturn(List.of(notificationResponse1, notificationResponse2, notificationResponse3)); //when ResultActions perform = mockMvc.perform( @@ -44,7 +49,23 @@ void findNotificationsByMemberId() throws Exception { perform.andDo(print()) .andDo(document("notification/findNotificationsByMemberId/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("회원 알림 조회") + .description("해당 회원의 알림 전체를 가져온다.") + .pathParameters( + parameterWithName("memberId").description("회원 ID") + ) + .responseFields( + fieldWithPath("[].notificationId").description("알림 ID"), + fieldWithPath("[].memberId").description("회원 ID"), + fieldWithPath("[].event").description("알림 이벤트 종류"), + fieldWithPath("[].message").description("알림 내용"), + fieldWithPath("[].checked").description("알림 확인 여부"), + fieldWithPath("[].createdAt").description("알림 생성 시간") + ) + .build()) + )); } @Test @@ -68,7 +89,23 @@ void markAllNotificationsAsCheckedForMember() throws Exception { perform.andDo(print()) .andDo(document("notification/markAllNotificationsAsCheckedForMember/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("알림 전체 확인") + .description("알림 버튼이 클릭되면 해당 회원의 전체 알림을 확인된 것으로 변경한다.") + .pathParameters( + parameterWithName("memberId").description("회원 ID") + ) + .responseFields( + fieldWithPath("[].notificationId").description("알림 ID"), + fieldWithPath("[].memberId").description("회원 ID"), + fieldWithPath("[].event").description("알림 이벤트 종류"), + fieldWithPath("[].message").description("알림 내용"), + fieldWithPath("[].checked").description("알림 확인 여부"), + fieldWithPath("[].createdAt").description("알림 생성 시간") + ) + .build()) + )); } } \ No newline at end of file diff --git a/backend/src/test/java/com/bootme/post/controller/PostControllerTest.java b/backend/src/test/java/com/bootme/post/controller/PostControllerTest.java index 439e7327..92de6aa6 100644 --- a/backend/src/test/java/com/bootme/post/controller/PostControllerTest.java +++ b/backend/src/test/java/com/bootme/post/controller/PostControllerTest.java @@ -1,8 +1,10 @@ package com.bootme.post.controller; +import com.bootme.comment.dto.CommentResponse; import com.bootme.common.util.CustomPageImpl; import com.bootme.post.dto.PostResponse; import com.bootme.util.ControllerTest; +import com.epages.restdocs.apispec.ResourceSnippetParameters; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -14,7 +16,10 @@ import java.util.Arrays; import java.util.List; +import static com.bootme.util.fixture.CommentFixture.getCommentResponse; import static com.bootme.util.fixture.PostFixture.*; +import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.*; import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; @@ -22,6 +27,7 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -49,14 +55,24 @@ void addPost() throws Exception { perform.andDo(print()) .andDo(document("posts/add/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("게시글 작성") + .description("커뮤니티 페이지에 게시글을 작성한다.") + .requestFields( + fieldWithPath("topic").description("게시글 토픽"), + fieldWithPath("title").description("게시글 제목"), + fieldWithPath("content").description("게시글 내용") + ) + .build()) + )); } @Test @DisplayName("findPost()는 정상 요청시 상태코드 200을 반환한다.") void findPost() throws Exception { //given - given(postService.findPost(anyLong(), anyLong())).willReturn(getPostDetailResponse(1)); + given(postService.findPost(any(), any())).willReturn(getPostDetailResponse(1)); //when ResultActions perform = mockMvc.perform(get("/posts/{id}", 1) @@ -69,7 +85,33 @@ void findPost() throws Exception { perform.andDo(print()) .andDo(document("posts/find/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("게시글 조회") + .description("게시글 상세 페이지에서 호출되어 해당 게시글 정보를 가져온다.") + .pathParameters( + parameterWithName("id").description("게시글 ID") + ) + .responseFields( + fieldWithPath("id").description("게시글 ID"), + fieldWithPath("writerId").description("작성자 ID"), + fieldWithPath("writerNickname").description("작성자 닉네임"), + fieldWithPath("writerProfileImage").description("작성자 프로필 이미지"), + fieldWithPath("topic").description("게시글 토픽"), + fieldWithPath("title").description("게시글 제목"), + fieldWithPath("content").description("게시글 내용"), + fieldWithPath("likes").description("게시글 vote"), + fieldWithPath("clicks").description("게시글 클릭 수"), + fieldWithPath("bookmarks").description("게시글 북마크 수"), + fieldWithPath("status").description("게시글 디스플레이 상태"), + fieldWithPath("createdAt").description("게시글 생성 시간"), + fieldWithPath("modifiedAt").description("게시글 수정 시간"), + fieldWithPath("commentCount").description("게시글의 댓글 수"), + fieldWithPath("voted").description("로그인 회원의 해당 게시물에 대한 vote 상태"), + fieldWithPath("bookmarked").description("로그인 회원의 해당 게시글 북마크 여부") + ) + .build()) + )); } @Test @@ -84,7 +126,7 @@ void findAllPosts() throws Exception { Page postPage = new PageImpl<>(postResponses); CustomPageImpl customPage = new CustomPageImpl<>(postPage); - given(postService.findAllPosts(anyLong(), anyInt(), anyInt(), anyString(), any())).willReturn(customPage); + given(postService.findAllPosts(any(), anyInt(), anyInt(), any(), any())).willReturn(customPage); //when ResultActions perform = mockMvc.perform(get("/posts") @@ -97,7 +139,99 @@ void findAllPosts() throws Exception { perform.andDo(print()) .andDo(document("posts/findAll/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("게시글 전체 조회") + .description("커뮤니티 페이지에서 디스플레이할 게시글 전체 정보를 가져온다.") + .queryParameters( + parameterWithName("page").description("페이지").optional(), + parameterWithName("size").description("한 번에 가져올 게시글 수").optional(), + parameterWithName("sort").description("정렬").optional(), + parameterWithName("topic").description("게시글 토픽").optional(), + parameterWithName("search").description("검색 쿼리").optional() + ) + .responseFields( + fieldWithPath("content[].id").description("게시글 ID"), + fieldWithPath("content[].writerId").description("작성자 ID"), + fieldWithPath("content[].writerNickname").description("작성자 닉네임"), + fieldWithPath("content[].writerProfileImage").description("작성자 프로필 이미지"), + fieldWithPath("content[].topic").description("게시글 토픽"), + fieldWithPath("content[].title").description("게시글 제목"), + fieldWithPath("content[].contentExcerpt").description("게시글 내용"), + fieldWithPath("content[].likes").description("게시글 vote"), + fieldWithPath("content[].clicks").description("게시글 클릭 수"), + fieldWithPath("content[].bookmarks").description("게시글 북마크 수"), + fieldWithPath("content[].status").description("게시글 디스플레이 상태"), + fieldWithPath("content[].createdAt").description("게시글 생성 시간"), + fieldWithPath("content[].modifiedAt").description("게시글 수정 시간"), + fieldWithPath("content[].commentCount").description("게시글의 댓글 수"), + fieldWithPath("content[].voted").description("로그인 회원의 해당 게시물에 대한 vote 상태"), + fieldWithPath("content[].bookmarked").description("로그인 회원의 해당 게시글 북마크 여부"), + fieldWithPath("content[].viewed").description("로그인 회원의 해당 게시글 조회 여부"), + fieldWithPath("last").description("마지막 페이지"), + fieldWithPath("totalPages").description("총 페이지 수"), + fieldWithPath("totalElements").description("총 아이템 수"), + fieldWithPath("size").description("크기"), + fieldWithPath("number").description("번호"), + fieldWithPath("sort.empty").description("정렬 비어 있음"), + fieldWithPath("sort.sorted").description("정렬 됨"), + fieldWithPath("sort.unsorted").description("미정렬"), + fieldWithPath("first").description("첫 페이지"), + fieldWithPath("numberOfElements").description("아이템 수"), + fieldWithPath("empty").description("비어 있음") + ) + .build()) + )); + } + + @Test + @DisplayName("findCommentsByPostId()는 정상 요청시 상태코드 200을 반환한다.") + void findCommentsByPostId() throws Exception { + //given + List commentResponses = Arrays.asList( + getCommentResponse(1), + getCommentResponse(2), + getCommentResponse(3) + ); + given(postService.findCommentsByPostId(any(), any())).willReturn(commentResponses); + + //when + ResultActions perform = mockMvc.perform(get("/posts/{id}/comments", 1) + .accept(MediaType.APPLICATION_JSON)); + + //then + perform.andExpect(status().isOk()); + + //docs + perform.andDo(print()) + .andDo(document("posts/findCommentsByPostId/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("게시글 댓글 조회") + .description("게시글 상세 페이지에서 호출되어 해당 게시글의 댓글들을 가져온다.") + .pathParameters( + parameterWithName("id").description("게시글 ID") + ) + .responseFields( + fieldWithPath("[].id").description("댓글 ID"), + fieldWithPath("[].postId").description("게시글 ID"), + fieldWithPath("[].writerId").description("작성자 ID"), + fieldWithPath("[].writerNickname").description("작성자 닉네임"), + fieldWithPath("[].writerProfileImage").description("작성자 프로필 이미지"), + fieldWithPath("[].parentId").description("부모 댓글 ID"), + fieldWithPath("[].content").description("댓글 내용"), + fieldWithPath("[].groupNum").description("댓글 그룹 번호"), + fieldWithPath("[].levelNum").description("댓글 레벨 번호"), + fieldWithPath("[].orderNum").description("댓글 순서 번호"), + fieldWithPath("[].likes").description("댓글 좋아요 수"), + fieldWithPath("[].voted").description("로그인 회원의 해당 댓글에 대한 vote 상태"), + fieldWithPath("[].status").description("댓글 디스플레이 상태"), + fieldWithPath("[].createdAt").description("댓글 생성 시간"), + fieldWithPath("[].modifiedAt").description("댓글 수정 시간") + ) + .build()) + )); } @Test @@ -119,7 +253,20 @@ void modifyPost() throws Exception{ perform.andDo(print()) .andDo(document("posts/modify/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("게시글 수정") + .description("게시글 수정") + .pathParameters( + parameterWithName("id").description("게시글 ID") + ) + .requestFields( + fieldWithPath("topic").description("게시글 토픽"), + fieldWithPath("title").description("게시글 제목"), + fieldWithPath("content").description("게시글 내용") + ) + .build()) + )); } @Test @@ -139,7 +286,15 @@ void deletePost() throws Exception { perform.andDo(print()) .andDo(document("posts/delete/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("게시글 삭제") + .description("해당 게시글의 status 칼럼 값을 DELETED로 변경한다.") + .pathParameters( + parameterWithName("id").description("게시글 ID") + ) + .build()) + )); } } diff --git a/backend/src/test/java/com/bootme/sse/SseControllerTest.java b/backend/src/test/java/com/bootme/sse/SseControllerTest.java index c43075e2..a2dddc6c 100644 --- a/backend/src/test/java/com/bootme/sse/SseControllerTest.java +++ b/backend/src/test/java/com/bootme/sse/SseControllerTest.java @@ -1,12 +1,14 @@ package com.bootme.sse; import com.bootme.util.ControllerTest; +import com.epages.restdocs.apispec.ResourceSnippetParameters; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.ResultActions; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; import static org.mockito.BDDMockito.willDoNothing; import static org.mockito.Mockito.any; import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; @@ -36,7 +38,12 @@ void connect() throws Exception { perform.andDo(print()) .andDo(document("sse/connect/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("SSE 연결") + .description("실시간 알림 송수신 위한 SSE 커넥션 연결") + .build()) + )); } } diff --git a/backend/src/test/java/com/bootme/stack/controller/StackControllerTest.java b/backend/src/test/java/com/bootme/stack/controller/StackControllerTest.java index 15b68ab6..c929d61f 100644 --- a/backend/src/test/java/com/bootme/stack/controller/StackControllerTest.java +++ b/backend/src/test/java/com/bootme/stack/controller/StackControllerTest.java @@ -3,6 +3,7 @@ import com.bootme.stack.dto.StackRequest; import com.bootme.stack.dto.StackResponse; import com.bootme.util.ControllerTest; +import com.epages.restdocs.apispec.ResourceSnippetParameters; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -12,10 +13,12 @@ import java.util.List; import static com.bootme.util.fixture.MemberFixture.*; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; import static org.mockito.BDDMockito.given; import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -42,7 +45,17 @@ void findAllStacks() throws Exception { perform.andDo(print()) .andDo(document("stacks/findAll/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("기술 스택 전체 조회") + .description("프로필 관리 페이지에서 기술 스택 설정을 위해 전체 기술 스택을 가져온다.") + .responseFields( + fieldWithPath("[].name").description("기술 스택 이름"), + fieldWithPath("[].type").description("기술 스택 유형"), + fieldWithPath("[].icon").description("기술 스택 아이콘") + ) + .build()) + )); } @Test @@ -65,77 +78,17 @@ void addStacks() throws Exception { perform.andDo(print()) .andDo(document("stacks/add/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); - } - - @Test - @DisplayName("addLanguage()는 정상 요청시 상태코드 200을 반환한다.") - void addLanguage() throws Exception { - // when - ResultActions perform = mockMvc.perform(post("/stacks/language") - .param("name", VALID_STACK_1) - .param("icon", VALID_STACK_ICON_1)); - - // then - perform.andExpect(status().isOk()); - - // docs - perform.andDo(print()) - .andDo(document("stacks/addLanguage/success", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); - } - - @Test - @DisplayName("removeLanguage()는 정상 요청시 상태코드 200을 반환한다.") - void removeLanguage() throws Exception { - // when - ResultActions perform = mockMvc.perform(delete("/stacks/language") - .param("name", VALID_STACK_2)); - - // then - perform.andExpect(status().isOk()); - - // docs - perform.andDo(print()) - .andDo(document("stacks/removeLanguage/success", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); - } - - @Test - @DisplayName("addFramework()는 정상 요청시 상태코드 200을 반환한다.") - void addFramework() throws Exception { - // when - ResultActions perform = mockMvc.perform(post("/stacks/framework") - .param("name", VALID_STACK_3) - .param("icon", VALID_STACK_ICON_3)); - - // then - perform.andExpect(status().isOk()); - - // docs - perform.andDo(print()) - .andDo(document("stacks/addFramework/success", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); - } - - @Test - @DisplayName("removeFramework()는 정상 요청시 상태코드 200을 반환한다.") - void removeFramework() throws Exception { - // when - ResultActions perform = mockMvc.perform(delete("/stacks/framework") - .param("name", VALID_STACK_4)); - - // then - perform.andExpect(status().isOk()); - - // docs - perform.andDo(print()) - .andDo(document("stacks/removeFramework/success", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("기술 스택 여러 개 추가") + .description("기술 스택 여러 개 추가") + .requestFields( + fieldWithPath("[].name").description("기술 스택 이름"), + fieldWithPath("[].type").description("기술 스택 유형"), + fieldWithPath("[].icon").description("기술 스택 아이콘") + ) + .build()) + )); } } diff --git a/backend/src/test/java/com/bootme/util/fixture/MemberFixture.java b/backend/src/test/java/com/bootme/util/fixture/MemberFixture.java index 79193b9f..b12308a9 100644 --- a/backend/src/test/java/com/bootme/util/fixture/MemberFixture.java +++ b/backend/src/test/java/com/bootme/util/fixture/MemberFixture.java @@ -108,7 +108,7 @@ public static StackResponse getStackResponse(int index) { public static ProfileResponse getProfileResponse(int index) { List stackResponses = new ArrayList<>(); stackResponses.add(getStackResponse(index)); - stackResponses.add(getStackResponse(index)); + stackResponses.add(getStackResponse(index+1)); index--; String[] profileImages = {VALID_PROFILE_IMAGE_1, VALID_PROFILE_IMAGE_2 ,VALID_PROFILE_IMAGE_3}; diff --git a/backend/src/test/java/com/bootme/util/fixture/VoteFixture.java b/backend/src/test/java/com/bootme/util/fixture/VoteFixture.java index 07b5f195..b3584864 100644 --- a/backend/src/test/java/com/bootme/util/fixture/VoteFixture.java +++ b/backend/src/test/java/com/bootme/util/fixture/VoteFixture.java @@ -11,11 +11,11 @@ public class VoteFixture { public static String VALID_VOTABLE_TYPE_1 = "POST"; public static String VALID_VOTABLE_TYPE_2 = "POST_COMMENT"; - public static VoteRequest getPostUpvoteRequest() { + public static VoteRequest getCommentUpvoteRequest() { return VoteRequest.builder() - .voteType(VALID_VOTE_TYPE_1) + .voteType(VALID_VOTABLE_TYPE_1) .votableId(1L) - .votableType(VALID_VOTABLE_TYPE_1) + .votableType(VALID_VOTABLE_TYPE_2) .memberId(1L) .build(); } diff --git a/backend/src/test/java/com/bootme/vote/controller/VoteControllerTest.java b/backend/src/test/java/com/bootme/vote/controller/VoteControllerTest.java index 805fdd1f..2f3de5b2 100644 --- a/backend/src/test/java/com/bootme/vote/controller/VoteControllerTest.java +++ b/backend/src/test/java/com/bootme/vote/controller/VoteControllerTest.java @@ -1,6 +1,7 @@ package com.bootme.vote.controller; import com.bootme.util.ControllerTest; +import com.epages.restdocs.apispec.ResourceSnippetParameters; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -9,11 +10,13 @@ import static com.bootme.util.fixture.CommentFixture.getCommentResponse; import static com.bootme.util.fixture.VoteFixture.*; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.*; import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -26,7 +29,7 @@ class VoteControllerTest extends ControllerTest { void vote() throws Exception { //given given(voteService.vote(any(), any())).willReturn(getCommentResponse(1)); - String content = objectMapper.writeValueAsString(getPostUpvoteRequest()); + String content = objectMapper.writeValueAsString(getCommentUpvoteRequest()); //when ResultActions perform = mockMvc.perform(post("/vote") @@ -40,7 +43,18 @@ void vote() throws Exception { perform.andDo(print()) .andDo(document("votes/vote/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("투표") + .description("게시글 혹은 댓글에 Upvote / Downvote") + .requestFields( + fieldWithPath("votableType").description("투표 대상 유형 (게시글 / 댓글)"), + fieldWithPath("votableId").description("투표 대상의 ID"), + fieldWithPath("voteType").description("Upvote / Downvote"), + fieldWithPath("memberId").description("회원 ID") + ) + .build()) + )); } } diff --git a/backend/src/test/java/com/bootme/webhook/controller/WebhookControllerTest.java b/backend/src/test/java/com/bootme/webhook/controller/WebhookControllerTest.java index f51c8985..57962ac5 100644 --- a/backend/src/test/java/com/bootme/webhook/controller/WebhookControllerTest.java +++ b/backend/src/test/java/com/bootme/webhook/controller/WebhookControllerTest.java @@ -2,6 +2,7 @@ import com.bootme.util.ControllerTest; import com.bootme.webhook.dto.WebhookRequest; +import com.epages.restdocs.apispec.ResourceSnippetParameters; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -13,11 +14,13 @@ import static com.bootme.common.exception.ErrorType.INVALID_EVENT; import static com.bootme.util.fixture.WebhookFixture.getWebhookRequest; +import static com.epages.restdocs.apispec.ResourceDocumentation.*; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.*; import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -47,7 +50,23 @@ void handleWebhook() throws Exception { perform.andDo(print()) .andDo(document("webhook/handleWebhook/success", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))); + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .summary("웹훅 처리") + .description("수신된 웹훅을 처리한다.") + .requestHeaders( + headerWithName("Bootme_Secret").description("BootMe 발행 JWT") + ) + .requestFields( + fieldWithPath("sessionId").description("세션 ID"), + fieldWithPath("memberId").description("회원 ID"), + fieldWithPath("event").description("웹훅 이벤트 유형"), + fieldWithPath("data.*").description("웹훅 이벤트 정보"), + fieldWithPath("currentUrl").description("웹훅 이벤트 발생한 페이지 URL"), + fieldWithPath("createdAt").description("웹훅 이벤트 발생 시간") + ) + .build()) + )); } @Test