From ab476ba25970cdc36407042b42ad24f2618d68d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=88=98=EB=B9=88?= <02ggang9@gmail.com> Date: Mon, 25 Mar 2024 10:25:18 +0900 Subject: [PATCH] =?UTF-8?q?Feature/#413=20rest=20docs=EB=A5=BC=20=EC=89=BD?= =?UTF-8?q?=EA=B2=8C=20=EC=9E=91=EC=84=B1=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=ED=95=9C=EB=8B=A4=20(#431)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: KEEPER-RestDocs 1차 구현 * feat: keeper-restDocs 2차 구현 * fest: keeper-restDocs 1차 테스트 * feat: 코틀린 DSL을 활용한 Spring Rest Docs 1차 구현 * fix: NPE 발생하는 문제 해결 * feat: KEEPER-RestDocs 페이징 기능 추가 * fix: GET 메서드 뿐만 아니라 다른 메서드도 사용할 수 있도록 수정 * fix: Documentation 어노테이션을 활용해 documentName을 작성하도록 수정 * feat: content 기능 추가 및 isOptional 기능 구현 * feat: pathParameter 기능 추가 * feat: Rest Docs 클래스 분리 * feat: DocsRequestBuilder 구현 * feat: DocsResponseBuilder 구현 * feat: KEEPER Rest Docs DSL 3차 구현 완료 * fix: Path 파라미터를 넣을 수 있도록 수정 * feat: 페이지네이션 기능 추가 * feat: Path 파라미터 기능 추가 * feat: param 기능 추가 구현 * test: 기존 Spring Rest Docs 코드를 KEEPER Spring Rest Docs 코드로 수정 * chore: 파일 구조 변경 * fix: 페이지네이션을 사용하는 경우 content[] 스트링을 적지 않도록 수정 * chore: 페이지네이션 테스트 코드에서 content[]. String 제거 * fix: util 클래스 삭제 * test: 기존의 MeritControllerTest 코드를 KEEPER DSL로 변환 * chore: 주석 제거 * chore: 테스트 Spring Rest Docs 삭제 --- .../com/keeper/homepage/IntegrationTest.java | 4 +- .../domain/merit/api/MeritControllerTest.java | 85 ++-- .../domain/merit/api/MeritControllerTest.kt | 421 ++++++++++++++++++ .../homepage/global/dsl/rest_docs/Doc.kt | 21 + .../global/dsl/rest_docs/DocsMethod.kt | 13 + .../global/dsl/rest_docs/Documentation.kt | 10 + .../homepage/global/dsl/rest_docs/Field.kt | 20 + .../dsl/rest_docs/RestDocsRequestBuilder.kt | 30 ++ .../rest_docs/builder/DocsRequestBuilder.kt | 42 ++ .../rest_docs/builder/DocsResponseBuilder.kt | 91 ++++ .../rest_docs/builder/DocsResultBuilder.kt | 19 + .../dsl/rest_docs/builder/RestDocsResult.kt | 50 +++ .../server/ThumbnailServerUtilTest.java | 29 +- 13 files changed, 792 insertions(+), 43 deletions(-) create mode 100644 src/test/java/com/keeper/homepage/domain/merit/api/MeritControllerTest.kt create mode 100644 src/test/java/com/keeper/homepage/global/dsl/rest_docs/Doc.kt create mode 100644 src/test/java/com/keeper/homepage/global/dsl/rest_docs/DocsMethod.kt create mode 100644 src/test/java/com/keeper/homepage/global/dsl/rest_docs/Documentation.kt create mode 100644 src/test/java/com/keeper/homepage/global/dsl/rest_docs/Field.kt create mode 100644 src/test/java/com/keeper/homepage/global/dsl/rest_docs/RestDocsRequestBuilder.kt create mode 100644 src/test/java/com/keeper/homepage/global/dsl/rest_docs/builder/DocsRequestBuilder.kt create mode 100644 src/test/java/com/keeper/homepage/global/dsl/rest_docs/builder/DocsResponseBuilder.kt create mode 100644 src/test/java/com/keeper/homepage/global/dsl/rest_docs/builder/DocsResultBuilder.kt create mode 100644 src/test/java/com/keeper/homepage/global/dsl/rest_docs/builder/RestDocsResult.kt diff --git a/src/test/java/com/keeper/homepage/IntegrationTest.java b/src/test/java/com/keeper/homepage/IntegrationTest.java index 51995a24e..0cab35621 100644 --- a/src/test/java/com/keeper/homepage/IntegrationTest.java +++ b/src/test/java/com/keeper/homepage/IntegrationTest.java @@ -457,7 +457,9 @@ protected void setUpAll(RestDocumentationContextProvider restDocumentationContex .apply(documentationConfiguration(restDocumentationContextProvider) .operationPreprocessors() .withRequestDefaults( - modifyUris().scheme("https").host("docs.api.com").removePort(), prettyPrint()) + modifyUris().scheme("https") + .host("docs.api.com") + .removePort(), prettyPrint()) .withResponseDefaults(prettyPrint()) ) .build(); diff --git a/src/test/java/com/keeper/homepage/domain/merit/api/MeritControllerTest.java b/src/test/java/com/keeper/homepage/domain/merit/api/MeritControllerTest.java index 8dcf7683e..674183364 100644 --- a/src/test/java/com/keeper/homepage/domain/merit/api/MeritControllerTest.java +++ b/src/test/java/com/keeper/homepage/domain/merit/api/MeritControllerTest.java @@ -195,22 +195,30 @@ void setUp() throws IOException { String securedValue = getSecuredValue(MeritController.class, "findMeritLogByMemberId"); meritLogTestHelper.builder() .memberId(member.getId()) - .meritType(meritTypeHelper.builder().merit(3).build()) + .meritType(meritTypeHelper.builder() + .merit(3) + .build()) .build(); meritLogTestHelper.builder() .memberId(member.getId()) - .meritType(meritTypeHelper.builder().merit(2).build()) + .meritType(meritTypeHelper.builder() + .merit(2) + .build()) .build(); meritLogTestHelper.builder() .memberId(member.getId()) - .meritType(meritTypeHelper.builder().merit(-1).build()) + .meritType(meritTypeHelper.builder() + .merit(-1) + .build()) .build(); meritLogTestHelper.builder() .memberId(member.getId()) - .meritType(meritTypeHelper.builder().merit(-3).build()) + .meritType(meritTypeHelper.builder() + .merit(-3) + .build()) .build(); mockMvc.perform( @@ -234,7 +242,9 @@ void setUp() throws IOException { @Test @DisplayName("일반회원은 회원별 상벌점 목록 조회를 할 수 없다.") void 일반회원은_회원별_상벌점_목록_조회를_할_수_없다() throws Exception { - meritLogTestHelper.builder().memberId(member.getId()).build(); + meritLogTestHelper.builder() + .memberId(member.getId()) + .build(); mockMvc.perform( get("/merits/members/{memberId}", member.getId()) @@ -277,40 +287,52 @@ void setUp() throws IOException { @DisplayName("벌점 목록 조회를 성공해야 한다.") void 벌점_목록_조회를_성공해야_한다() throws Exception { meritLogTestHelper.builder() - .memberId(member.getId()) - .meritType(meritTypeHelper.builder().merit(3).isMerit(true).build()) - .build(); + .memberId(member.getId()) + .meritType(meritTypeHelper.builder() + .merit(3) + .isMerit(true) + .build()) + .build(); meritLogTestHelper.builder() - .memberId(member.getId()) - .meritType(meritTypeHelper.builder().merit(-3).isMerit(false).build()) - .build(); + .memberId(member.getId()) + .meritType(meritTypeHelper.builder() + .merit(-3) + .isMerit(false) + .build()) + .build(); mockMvc.perform(get("/merits") - .param("meritType", "DEMERIT") - .cookie(new Cookie(ACCESS_TOKEN.getTokenName(), adminAccessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.content[0].isMerit").value("false")); + .param("meritType", "DEMERIT") + .cookie(new Cookie(ACCESS_TOKEN.getTokenName(), adminAccessToken))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content[0].isMerit").value("false")); } @Test @DisplayName("상점 목록 조회를 성공해야 한다.") void 상점_목록_조회를_성공해야_한다() throws Exception { meritLogTestHelper.builder() - .memberId(member.getId()) - .meritType(meritTypeHelper.builder().merit(-3).isMerit(false).build()) - .build(); + .memberId(member.getId()) + .meritType(meritTypeHelper.builder() + .merit(-3) + .isMerit(false) + .build()) + .build(); meritLogTestHelper.builder() - .memberId(member.getId()) - .meritType(meritTypeHelper.builder().merit(3).isMerit(true).build()) - .build(); + .memberId(member.getId()) + .meritType(meritTypeHelper.builder() + .merit(3) + .isMerit(true) + .build()) + .build(); mockMvc.perform(get("/merits") - .param("meritType", "MERIT") - .cookie(new Cookie(ACCESS_TOKEN.getTokenName(), adminAccessToken))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.content[0].isMerit").value("true")); + .param("meritType", "MERIT") + .cookie(new Cookie(ACCESS_TOKEN.getTokenName(), adminAccessToken))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content[0].isMerit").value("true")); } @Test @@ -340,7 +362,7 @@ void setUp() throws IOException { @Test @DisplayName("일반회원은 상벌점 부여 로그를 생성할 수 없다.") - void 일반회원은_상벌점_부여_로그를_생성할_수_없다() throws Exception { + void 일반회원은_상벌점_부여_로그를_생성할_수_없다() throws Exception { // o GiveMeritPointRequest request = GiveMeritPointRequest.builder() .awarderId(member.getId()) .meritTypeId(meritType.getId()) @@ -361,8 +383,12 @@ class GetAllTotalMeritLogsTest { @BeforeEach void setUp() { - meritType = meritTypeHelper.builder().merit(5).build(); - demeritType = meritTypeHelper.builder().merit(-3).build(); + meritType = meritTypeHelper.builder() + .merit(5) + .build(); + demeritType = meritTypeHelper.builder() + .merit(-3) + .build(); meritLogTestHelper.builder() .memberId(member.getId()) @@ -429,7 +455,8 @@ class DeleteMeritLogTest { @BeforeEach void setUp() { - meritLogId = meritLogTestHelper.generate().getId(); + meritLogId = meritLogTestHelper.generate() + .getId(); } @Test diff --git a/src/test/java/com/keeper/homepage/domain/merit/api/MeritControllerTest.kt b/src/test/java/com/keeper/homepage/domain/merit/api/MeritControllerTest.kt new file mode 100644 index 000000000..ce8bb5b3a --- /dev/null +++ b/src/test/java/com/keeper/homepage/domain/merit/api/MeritControllerTest.kt @@ -0,0 +1,421 @@ +package com.keeper.homepage.domain.merit.api + +import com.keeper.homepage.IntegrationTest +import com.keeper.homepage.domain.member.entity.Member +import com.keeper.homepage.domain.member.entity.job.MemberJob +import com.keeper.homepage.domain.merit.dto.request.AddMeritTypeRequest +import com.keeper.homepage.domain.merit.dto.request.GiveMeritPointRequest +import com.keeper.homepage.domain.merit.dto.request.UpdateMeritTypeRequest +import com.keeper.homepage.domain.merit.entity.MeritType +import com.keeper.homepage.global.config.security.data.JwtType.* +import com.keeper.homepage.global.dsl.rest_docs.Documentation +import com.keeper.homepage.global.dsl.rest_docs.DocsMethod +import com.keeper.homepage.global.dsl.rest_docs.DocsMethod.* +import com.keeper.homepage.global.dsl.rest_docs.docs +import com.keeper.homepage.global.dsl.rest_docs.means +import com.keeper.homepage.global.restdocs.RestDocsHelper.getSecuredValue +import io.jsonwebtoken.io.IOException +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.springframework.http.HttpMethod.* +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.* + +class MeritControllerTest1 : IntegrationTest() { + + private var meritType: MeritType? = null + private var demeritType: MeritType? = null + private var member: Member? = null + private var admin: Member? = null + private var otherMember: Member? = null + private var userAccessToken: String? = null + private var adminAccessToken: String? = null + + @BeforeEach + @Throws(IOException::class) + fun setUp() { + meritType = meritTypeHelper.generate() + member = memberTestHelper.generate() + otherMember = memberTestHelper.generate() + admin = memberTestHelper.generate().apply { assignJob(MemberJob.MemberJobType.ROLE_회장) } + userAccessToken = jwtTokenProvider.createAccessToken(ACCESS_TOKEN, member!!.id, + MemberJob.MemberJobType.ROLE_회원) + adminAccessToken = jwtTokenProvider.createAccessToken(ACCESS_TOKEN, member!!.id, + MemberJob.MemberJobType.ROLE_회장, MemberJob.MemberJobType.ROLE_부회장, MemberJob.MemberJobType.ROLE_서기, MemberJob.MemberJobType.ROLE_회원) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class MeritTypeTest { + + @DisplayName("상벌점 타입 조회를 성공해야 한다.") + @Documentation("search-meritType-kt") + fun `상벌점 조회는 성공해야 한다`() { + val securedValue = getSecuredValue(MeritController::class.java, "searchMeritType") + docs(mockMvc, DocsMethod.GET, "/merits/types") { + request { cookie(*memberTestHelper.getTokenCookies(admin)) } + result { + expect(status().isOk()) + expect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + } + response { + cookie( + ACCESS_TOKEN.tokenName means "ACCESS TOKEN $securedValue", + REFRESH_TOKEN.tokenName means "REFRESH TOKEN", + ) + responseBodyWithPaging( + "id" means "상벌점 타입의 ID", + "score" means "상벌점 점수", + "detail" means "상벌점 사유", + "isMerit" means "상벌점 타입", + ) + } + } + } + + @Test + fun `일반 회원은 조회할 수 없다`() { + docs(mockMvc, DocsMethod.GET, "/merits/types") { + request { cookie(*memberTestHelper.getTokenCookies(member!!)) } + result { + expect(status().isForbidden()) + expect(jsonPath("$.message").exists()) + } + } + } + + @Documentation("create-meritType-kt") + fun `상벌점 타입 생성을 성공해야 한다`() { + val securedValue = getSecuredValue(MeritController::class.java, "registerMeritType") + val request = AddMeritTypeRequest.builder() + .score(3) + .reason("우수기술문서 작성") + .isMerit(true) + .build() + + docs(mockMvc, DocsMethod.POST, "/merits/types") { + request { + cookie(*memberTestHelper.getTokenCookies(admin)) + content(asJsonString(request)) + contentType(MediaType.APPLICATION_JSON) + } + result { expect(status().isCreated()) } + response { + cookie( + ACCESS_TOKEN.tokenName means "ACCESS TOKEN $securedValue", + REFRESH_TOKEN.tokenName means "REFRESH TOKEN", + ) + requestBody( + "score" means "상벌점 점수를 입력해주세요.", + "reason" means "상벌점 사유를 입력해주세요.", + "isMerit" means "상벌점 타입를 입력해주세요.", + ) + } + } + } + + @Test + fun `일반 회원은 상벌점 타입을 생성할 수 없다`() { + val request = AddMeritTypeRequest.builder() + .score(3) + .reason("우수기술문서 작성") + .isMerit(true) + .build() + + docs(mockMvc, DocsMethod.POST, "/merits/types") { + request { + cookie(*memberTestHelper.getTokenCookies(member!!)) + content(asJsonString(request)) + contentType(MediaType.APPLICATION_JSON) + } + result { + expect(status().isForbidden()) + expect(jsonPath("$.message").exists()) + } + } + } + + @Documentation("update-meritType-kt") + fun `상벌점 부여 로그 수정을 성공해야 한다`() { + val securedValue = getSecuredValue(MeritController::class.java, "updateMeritType") + val request = UpdateMeritTypeRequest.builder() + .score(-5) + .reason("거짓 스터디") + .isMerit(false) + .build() + + docs(mockMvc, DocsMethod.PUT, "/merits/types/{meritTypeId}", "${meritType!!.id}") { + request { + cookie(*memberTestHelper.getTokenCookies(admin)) + content(asJsonString(request)) + contentType(MediaType.APPLICATION_JSON) + } + result { expect(status().isCreated()) } + response { + path("meritTypeId" means "수정하고자 하는 상벌점 타입의 ID") + cookie( + ACCESS_TOKEN.tokenName means "ACCESS TOKEN $securedValue", + REFRESH_TOKEN.tokenName means "REFRESH TOKEN", + ) + requestBody( + "score" means "상벌점 점수를 입력해주세요.", + "reason" means "상벌점 사유를 입력해주세요.", + "isMerit" means "상벌점 타입를 입력해주세요.", + ) + } + } + } + + @Test + fun `일반회원은 상벌점 타입 수정을 성공할 수 없다`() { + val request = UpdateMeritTypeRequest.builder() + .score(-5) + .reason("거짓 스터디") + .isMerit(false) + .build() + + docs(mockMvc, DocsMethod.PUT, "/merits/types/{meritTypeId}", "${meritType!!.id}") { + request { + cookie(*memberTestHelper.getTokenCookies(member!!)) + content(asJsonString(request)) + contentType(MediaType.APPLICATION_JSON) + } + result { + expect(status().isForbidden()) + expect(jsonPath("$.message").exists()) + } + } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class MeritLogTest { + + @BeforeEach + fun setUp() { + meritLogTestHelper.generate() + meritLogTestHelper.generate() + } + + @Documentation("search-member-meritLog") + fun `회원별 상벌점 목록 조회를 성공해야 한다`() { + val securedValue = getSecuredValue(MeritController::class.java, "findMeritLogByMemberId") + meritLogTestHelper.builder() + .memberId(member!!.id) + .meritType(meritTypeHelper.builder() + .merit(3) + .build()) + .build() + + meritLogTestHelper.builder() + .memberId(member!!.id) + .meritType(meritTypeHelper.builder() + .merit(2) + .build()) + .build() + + meritLogTestHelper.builder() + .memberId(member!!.id) + .meritType(meritTypeHelper.builder() + .merit(-1) + .build()) + .build() + + meritLogTestHelper.builder() + .memberId(member!!.id) + .meritType(meritTypeHelper.builder() + .merit(-3) + .build()) + .build() + + docs(mockMvc, DocsMethod.GET, "/merits/members/{memberId}", "${member!!.id}") { + request { cookie(*memberTestHelper.getTokenCookies(admin)) } + result { + expect(status().isOk()) + expect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + } + response { + path("memberId" means "조회하고자 하는 멤버의 ID 값") + cookie( + ACCESS_TOKEN.tokenName means "ACCESS TOKEN $securedValue", + REFRESH_TOKEN.tokenName means "REFRESH TOKEN", + ) + responseBodyWithPaging( + "id" means "상벌점 로그의 ID", + "giveTime" means "상벌점 로그의 생성시간", + "score" means "상벌점 점수", + "meritTypeId" means "상벌점 타입의 ID", + "reason" means "상벌점 사유", + "isMerit" means "상벌점 타입", + ) + } + } + } + + @Test + fun `일반회원은 회원별 상벌점 목록 조회를 할 수 없다`() { + meritLogTestHelper.builder() + .memberId(member!!.id) + .build() + + docs(mockMvc, DocsMethod.GET, "/merits/members/{memberId}", "${member!!.id}") { + request { cookie(*memberTestHelper.getTokenCookies(member!!)) } + result { + expect(status().isForbidden()) + expect(jsonPath("$.message").exists()) + } + } + } + + @Documentation("search-meritLog-kt") + fun `상벌점 목록 조회를 성공해야 한다`() { + val securedValue = getSecuredValue(MeritController::class.java, "searchMeritLogList") + docs(mockMvc, DocsMethod.GET, "/merits") { + request { cookie(*memberTestHelper.getTokenCookies(admin)) } + result { + expect(status().isOk()) + expect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + } + response { + cookie( + ACCESS_TOKEN.tokenName means "ACCESS TOKEN $securedValue", + REFRESH_TOKEN.tokenName means "REFRESH TOKEN", + ) + responseBodyWithPaging( + "id" means "상벌점 로그의 ID", + "giveTime" means "상벌점 로그의 생성시간", + "score" means "상벌점 점수", + "meritTypeId" means "상벌점 타입의 ID", + "reason" means "상벌점 사유", + "isMerit" means "상벌점 타입", + "awarderName" means "수상자의 이름", + "awarderGeneration" means "수상자의 학번", + ) + } + } + } + + @Test + fun `일반회원은 상벌점 목록 조회를 할 수 없다`() { + docs(mockMvc, DocsMethod.GET, "/merits") { + request { cookie(*memberTestHelper.getTokenCookies(member!!)) } + result { + expect(status().isForbidden()) + expect(jsonPath("$.message").exists()) + } + } + } + + @Test + fun `벌점 목록 조회를 성공해야 한다`() { + meritLogTestHelper.builder() + .memberId(member!!.id) + .meritType(meritTypeHelper.builder() + .merit(3) + .isMerit(true) + .build()) + .build() + + meritLogTestHelper.builder() + .memberId(member!!.id) + .meritType(meritTypeHelper.builder() + .merit(-3) + .isMerit(false) + .build()) + .build() + + docs(mockMvc, DocsMethod.GET, "/merits") { + request { + param("meritType", "DEMERIT") + cookie(*memberTestHelper.getTokenCookies(admin!!)) + } + result { + expect(status().isOk()) + expect(jsonPath("$.content[0].isMerit").value("false")) + } + } + } + + @Test + fun `상점 목록 조회를 성공해야 한다`() { + meritLogTestHelper.builder() + .memberId(member!!.id) + .meritType(meritTypeHelper.builder() + .merit(-3) + .isMerit(false) + .build()) + .build() + + meritLogTestHelper.builder() + .memberId(member!!.id) + .meritType(meritTypeHelper.builder() + .merit(3) + .isMerit(true) + .build()) + .build() + + docs(mockMvc, DocsMethod.GET, "/merits") { + request { + param("meritType", "MERIT") + cookie(*memberTestHelper.getTokenCookies(admin!!)) + } + result { + expect(status().isOk()) + expect(jsonPath("$.content[0].isMerit").value("true")) + } + } + } + + @Documentation("create-meritLog-kt") + fun `상벌점 부여 로그 생성을 성공해야 한다`() { + val securedValue = getSecuredValue(MeritController::class.java, "registerMerit") + val request = GiveMeritPointRequest.builder() + .awarderId(member!!.id) + .meritTypeId(meritType!!.id) + .build() + + docs(mockMvc, MULTIPART, "/merits") { + request { + cookie(*memberTestHelper.getTokenCookies(admin)) + content(asJsonString(request)) + contentType(MediaType.APPLICATION_JSON) + } + result { expect(status().isCreated()) } + response { + cookie( + ACCESS_TOKEN.tokenName means "ACCESS TOKEN $securedValue", + REFRESH_TOKEN.tokenName means "REFRESH TOKEN", + ) + requestBody( + "awarderId" means "수여자의 ID", + "meritTypeId" means "상벌점 타입의 ID", + ) + } + } + } + + @Test + fun `일반회원은 상벌점 부여 로그를 생성할 수 없다`() { + val request = GiveMeritPointRequest.builder() + .awarderId(member!!.id) + .meritTypeId(meritType!!.id) + .build() + + docs(mockMvc, MULTIPART, "/merits") { + request { + cookie(*memberTestHelper.getTokenCookies(member!!)) + content(asJsonString(request)) + contentType(MediaType.APPLICATION_JSON) + } + result { + expect(status().isForbidden()) + expect(jsonPath("$.message").exists()) + } + } + } + } +} \ No newline at end of file diff --git a/src/test/java/com/keeper/homepage/global/dsl/rest_docs/Doc.kt b/src/test/java/com/keeper/homepage/global/dsl/rest_docs/Doc.kt new file mode 100644 index 000000000..2c375d181 --- /dev/null +++ b/src/test/java/com/keeper/homepage/global/dsl/rest_docs/Doc.kt @@ -0,0 +1,21 @@ +package com.keeper.homepage.global.dsl.rest_docs + +import com.keeper.homepage.global.dsl.rest_docs.builder.RestDocsResult +import org.springframework.test.web.servlet.MockMvc + +fun docs( + mockMvc: MockMvc, + method: DocsMethod, + url: String, + vararg pathParams: String, + init: RestDocsResult.() -> Unit +): RestDocsRequestBuilder { + val restDocsRequestBuilder = RestDocsRequestBuilder(mockMvc, method, url, pathParams) + val requestBuilder = restDocsRequestBuilder.build()!! + + val restDocsResult = RestDocsResult(mockMvc, requestBuilder) + restDocsResult.init() + restDocsResult.generateDocs() + + return restDocsRequestBuilder +} \ No newline at end of file diff --git a/src/test/java/com/keeper/homepage/global/dsl/rest_docs/DocsMethod.kt b/src/test/java/com/keeper/homepage/global/dsl/rest_docs/DocsMethod.kt new file mode 100644 index 000000000..e85f554f6 --- /dev/null +++ b/src/test/java/com/keeper/homepage/global/dsl/rest_docs/DocsMethod.kt @@ -0,0 +1,13 @@ +package com.keeper.homepage.global.dsl.rest_docs + +enum class DocsMethod( + val method: String +) { + GET("GET"), + POST("POST"), + PUT("PUT"), + DELETE("DELETE"), + PATCH("PATCH"), + MULTIPART("MULTIPART"), + +} diff --git a/src/test/java/com/keeper/homepage/global/dsl/rest_docs/Documentation.kt b/src/test/java/com/keeper/homepage/global/dsl/rest_docs/Documentation.kt new file mode 100644 index 000000000..895cafd91 --- /dev/null +++ b/src/test/java/com/keeper/homepage/global/dsl/rest_docs/Documentation.kt @@ -0,0 +1,10 @@ +package com.keeper.homepage.global.dsl.rest_docs + +import org.junit.jupiter.api.Test + +@Test +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class Documentation( + val documentName: String, +) diff --git a/src/test/java/com/keeper/homepage/global/dsl/rest_docs/Field.kt b/src/test/java/com/keeper/homepage/global/dsl/rest_docs/Field.kt new file mode 100644 index 000000000..83f675767 --- /dev/null +++ b/src/test/java/com/keeper/homepage/global/dsl/rest_docs/Field.kt @@ -0,0 +1,20 @@ +package com.keeper.homepage.global.dsl.rest_docs + +import com.keeper.homepage.domain.library.api.field + +open class Field( + var fieldName: String, + val description: String?, + val isOptional: Boolean?, +) { + constructor(field: Field, isOptional: Boolean) : this(field.fieldName, field.description, isOptional) + + fun addContentString() { + fieldName = "content[].$fieldName" + } + +} + +infix fun String.means(description: String): Field { + return Field(this, description, false) +} \ No newline at end of file diff --git a/src/test/java/com/keeper/homepage/global/dsl/rest_docs/RestDocsRequestBuilder.kt b/src/test/java/com/keeper/homepage/global/dsl/rest_docs/RestDocsRequestBuilder.kt new file mode 100644 index 000000000..3e62d8e51 --- /dev/null +++ b/src/test/java/com/keeper/homepage/global/dsl/rest_docs/RestDocsRequestBuilder.kt @@ -0,0 +1,30 @@ +package com.keeper.homepage.global.dsl.rest_docs + +import jakarta.servlet.http.Cookie +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.ResultActions +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder +import org.springframework.util.Assert +import java.net.URI + +class RestDocsRequestBuilder( + private val mockMvc: MockMvc, + private val method: DocsMethod, + private val url: String, + private val pathParams: Array<*>? = null +) { + + fun build(): MockHttpServletRequestBuilder? { + val mockRequestBuilder = when (method) { + DocsMethod.GET -> RestDocumentationRequestBuilders.get(url, pathParams?.joinToString(", ")) + DocsMethod.PUT -> RestDocumentationRequestBuilders.put(url, pathParams?.joinToString(", ")) + DocsMethod.POST -> RestDocumentationRequestBuilders.post(url, pathParams?.joinToString(", ")) + DocsMethod.DELETE -> RestDocumentationRequestBuilders.delete(url, pathParams?.joinToString(", ")) + DocsMethod.PATCH -> RestDocumentationRequestBuilders.patch(url, pathParams?.joinToString(", ")) + DocsMethod.MULTIPART -> RestDocumentationRequestBuilders.multipart(url, pathParams?.joinToString(", ")) + } + + return mockRequestBuilder + } +} \ No newline at end of file diff --git a/src/test/java/com/keeper/homepage/global/dsl/rest_docs/builder/DocsRequestBuilder.kt b/src/test/java/com/keeper/homepage/global/dsl/rest_docs/builder/DocsRequestBuilder.kt new file mode 100644 index 000000000..a472f5453 --- /dev/null +++ b/src/test/java/com/keeper/homepage/global/dsl/rest_docs/builder/DocsRequestBuilder.kt @@ -0,0 +1,42 @@ +package com.keeper.homepage.global.dsl.rest_docs.builder + +import jakarta.servlet.http.Cookie +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.ResultActions +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder +import org.springframework.util.Assert + +class DocsRequestBuilder { + + private val cookies: MutableList = mutableListOf() + private var content: String? = null + private var contentType: String? = null + private var paramMap: MutableMap = mutableMapOf() + + fun content(content: String) { + this.content = content + } + + fun contentType(contentType: MediaType) { + this.contentType = contentType.toString() + } + + fun cookie(vararg cookies: Cookie?) { + Assert.notNull(cookies, "Cookies must not be null") + cookies.filterNotNull().forEach { this.cookies.add(it) } + } + + fun param(name: String, value: String) { + paramMap[name] = value + } + + fun build(mockMvc: MockMvc, resultActions: MockHttpServletRequestBuilder): ResultActions { + cookies.forEach { resultActions.cookie(it) } + paramMap.forEach { (name, value) -> resultActions.param(name, value) } + content?.let { resultActions.content(it) } + contentType?.let { resultActions.contentType(it) } + return mockMvc.perform(resultActions) + } + +} \ No newline at end of file diff --git a/src/test/java/com/keeper/homepage/global/dsl/rest_docs/builder/DocsResponseBuilder.kt b/src/test/java/com/keeper/homepage/global/dsl/rest_docs/builder/DocsResponseBuilder.kt new file mode 100644 index 000000000..2777823b4 --- /dev/null +++ b/src/test/java/com/keeper/homepage/global/dsl/rest_docs/builder/DocsResponseBuilder.kt @@ -0,0 +1,91 @@ +package com.keeper.homepage.global.dsl.rest_docs.builder + +import com.keeper.homepage.global.dsl.rest_docs.Documentation +import com.keeper.homepage.global.dsl.rest_docs.Field +import com.keeper.homepage.global.dsl.rest_docs.means +import org.springframework.restdocs.cookies.CookieDocumentation +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation +import org.springframework.restdocs.payload.PayloadDocumentation +import org.springframework.restdocs.request.RequestDocumentation +import org.springframework.restdocs.snippet.Snippet +import org.springframework.test.web.servlet.ResultActions + +class DocsResponseBuilder { + + private val responseFields: MutableList = mutableListOf() + private val requestFields: MutableList = mutableListOf() + private val cookieFields: MutableList = mutableListOf() + private val pathFields: MutableList = mutableListOf() + private var pageable: Boolean = false + + fun path(vararg fields: Field) { + pathFields.addAll(fields) + } + + fun cookie(vararg cookies: Field) { + cookieFields.addAll(cookies) + } + + fun requestBody(vararg fields: Field) { + requestFields.addAll(fields) + } + + fun responseBody(vararg fields: Field) { + responseFields.addAll(fields) + } + + fun responseBodyWithPaging(vararg fields: Field) { + pageable = true + fields.forEach { it.addContentString() } + responseFields.addAll(fields) + responseFields.add("empty" means "가져오는 페이지가 비어 있는 지") + responseFields.add("first" means "가져오는 페이지가 비어 있는 지") + responseFields.add("last" means "가져오는 페이지가 비어 있는 지") + responseFields.add("number" means "가져오는 페이지가 비어 있는 지") + responseFields.add("numberOfElements" means "가져오는 페이지가 비어 있는 지") + responseFields.add("sort.empty" means "가져오는 페이지가 비어 있는 지") + responseFields.add("sort.sorted" means "가져오는 페이지가 비어 있는 지") + responseFields.add("sort.unsorted" means "가져오는 페이지가 비어 있는 지") + responseFields.add("totalPages" means "가져오는 페이지가 비어 있는 지") + responseFields.add("totalElements" means "가져오는 페이지가 비어 있는 지") + responseFields.add("size" means "가져오는 페이지가 비어 있는 지") + } + + fun build(mockMvc: ResultActions) { + val currentStackTraceElement = Thread.currentThread().stackTrace.firstOrNull { it.fileName?.endsWith("Test.kt") == true } + val className = currentStackTraceElement?.className + val methodName = currentStackTraceElement?.methodName + + val method = Class.forName(className).methods.firstOrNull { it.name == methodName } + val documentName = method?.getAnnotation(Documentation::class.java)?.documentName + + val cookieFieldDescriptors = cookieFields.map { CookieDocumentation.cookieWithName(it.fieldName).description(it.description) } + val pathParameterDescriptors = pathFields.map { RequestDocumentation.parameterWithName(it.fieldName).description(it.description) } + val requestFieldDescriptors = requestFields.map { PayloadDocumentation.fieldWithPath(it.fieldName).description(it.description) } + val responseFieldDescriptors = responseFields.map { PayloadDocumentation.fieldWithPath(it.fieldName).description(it.description) }.toMutableList() + + if (pageable) { + responseFieldDescriptors.add(PayloadDocumentation.subsectionWithPath("pageable").description("페이지에 대한 부가 정보")) + } + + val cookieFieldsSnippet = CookieDocumentation.requestCookies(*cookieFieldDescriptors.toTypedArray()) + val pathParametersSnippet = RequestDocumentation.pathParameters(*pathParameterDescriptors.toTypedArray()) + val requestFieldsSnippet = PayloadDocumentation.requestFields(*requestFieldDescriptors.toTypedArray()) + val responseFieldsSnippet = PayloadDocumentation.responseFields(*responseFieldDescriptors.toTypedArray()) + + if (pageable) { + responseFieldsSnippet.apply { PayloadDocumentation.subsectionWithPath("pageable").description("페이지에 대한 부가 정보") } + } + + val snippets = mutableListOf().apply { + if (pathParameterDescriptors.isNotEmpty()) { add(pathParametersSnippet) } + if (cookieFieldDescriptors.isNotEmpty()) { add(cookieFieldsSnippet) } + if (requestFieldDescriptors.isNotEmpty()) { add(requestFieldsSnippet) } + if (responseFieldDescriptors.isNotEmpty()) { add(responseFieldsSnippet) } + } + + mockMvc.andDo( + MockMvcRestDocumentation.document(documentName, *snippets.toTypedArray()) + ) + } +} \ No newline at end of file diff --git a/src/test/java/com/keeper/homepage/global/dsl/rest_docs/builder/DocsResultBuilder.kt b/src/test/java/com/keeper/homepage/global/dsl/rest_docs/builder/DocsResultBuilder.kt new file mode 100644 index 000000000..e26e492f4 --- /dev/null +++ b/src/test/java/com/keeper/homepage/global/dsl/rest_docs/builder/DocsResultBuilder.kt @@ -0,0 +1,19 @@ +package com.keeper.homepage.global.dsl.rest_docs.builder + +import org.springframework.test.web.servlet.ResultActions +import org.springframework.test.web.servlet.ResultMatcher + +class DocsResultBuilder { + + private val resultMatcher: MutableList = mutableListOf() + + fun expect(result: ResultMatcher) { + resultMatcher.add(result) + } + + fun build(mockMvc: ResultActions): ResultActions { + resultMatcher.forEach { mockMvc.andExpect(it) } + return mockMvc + } + +} \ No newline at end of file diff --git a/src/test/java/com/keeper/homepage/global/dsl/rest_docs/builder/RestDocsResult.kt b/src/test/java/com/keeper/homepage/global/dsl/rest_docs/builder/RestDocsResult.kt new file mode 100644 index 000000000..12b7b1ae8 --- /dev/null +++ b/src/test/java/com/keeper/homepage/global/dsl/rest_docs/builder/RestDocsResult.kt @@ -0,0 +1,50 @@ +package com.keeper.homepage.global.dsl.rest_docs.builder + +import com.keeper.homepage.global.dsl.rest_docs.Documentation +import com.keeper.homepage.global.dsl.rest_docs.Field +import jakarta.servlet.http.Cookie +import org.springframework.http.MediaType +import org.springframework.restdocs.cookies.CookieDocumentation.* +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document +import org.springframework.restdocs.payload.PayloadDocumentation.* +import org.springframework.restdocs.request.RequestDocumentation.parameterWithName +import org.springframework.restdocs.request.RequestDocumentation.pathParameters +import org.springframework.restdocs.snippet.Snippet +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.ResultActions +import org.springframework.test.web.servlet.ResultMatcher +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder +import org.springframework.util.Assert + +class RestDocsResult( + private val mockMvc: MockMvc, + private val resultActions: MockHttpServletRequestBuilder, +) { + private lateinit var request: DocsRequestBuilder + private lateinit var results : DocsResultBuilder + private var responses : DocsResponseBuilder? = null + + fun request(init: DocsRequestBuilder.() -> Unit) { + val docsRequestBuilder = DocsRequestBuilder() + docsRequestBuilder.init() + this.request = docsRequestBuilder + } + + fun result(init: DocsResultBuilder.() -> Unit) { + val docsResultBuilder = DocsResultBuilder() + docsResultBuilder.init() + results = docsResultBuilder + } + + fun response(init: DocsResponseBuilder.() -> Unit) { + val docsResponseBuilder = DocsResponseBuilder() + docsResponseBuilder.init() + this.responses = docsResponseBuilder + } + + fun generateDocs() { + val mock = request.build(mockMvc, resultActions) + val build = results.build(mock) + responses?.let { it.build(build) } + } +} diff --git a/src/test/java/com/keeper/homepage/global/util/thumbnail/server/ThumbnailServerUtilTest.java b/src/test/java/com/keeper/homepage/global/util/thumbnail/server/ThumbnailServerUtilTest.java index fc1d5964e..9da05c8b5 100644 --- a/src/test/java/com/keeper/homepage/global/util/thumbnail/server/ThumbnailServerUtilTest.java +++ b/src/test/java/com/keeper/homepage/global/util/thumbnail/server/ThumbnailServerUtilTest.java @@ -31,8 +31,10 @@ void should_deleteSuccessfully_when_existFile() throws IOException { MockMultipartFile validMultipartFile = new MockMultipartFile("image", "testImage_210x210.png", "image/png", new FileInputStream("src/test/resources/images/testImage_210x210.png")); - Thumbnail savedThumbnail = thumbnailUtil.saveThumbnail(validMultipartFile).orElseThrow(); - String filePath = savedThumbnail.getFileEntity().getFilePath(); + Thumbnail savedThumbnail = thumbnailUtil.saveThumbnail(validMultipartFile) + .orElseThrow(); + String filePath = savedThumbnail.getFileEntity() + .getFilePath(); Path fileFullPath = Path.of(ROOT_PATH + separator + filePath); Path thumbnailFullPath = Path.of(ROOT_PATH + separator + savedThumbnail.getPath()); assertThat(Files.exists(fileFullPath)).isTrue(); @@ -60,7 +62,8 @@ class SaveTest { @Test @DisplayName("썸네일 파일이 유효한 경우 저장은 성공해야 한다.") void should_saveSuccessfully_when_validMultipartFile() { - Thumbnail result = thumbnailUtil.saveThumbnail(validMultipartFile).orElseThrow(); + Thumbnail result = thumbnailUtil.saveThumbnail(validMultipartFile) + .orElseThrow(); assertThat(result).isNotNull(); deleteTestFile(result); } @@ -81,16 +84,16 @@ void should_resizingWell_when_validMultipartFile() throws IOException { deleteTestFile(savedThumbnailEntity); } - @Test - @DisplayName("썸네일 저장 시 type이 null이면 NullPointerException을 발생시킨다.") - void should_throwNullPointerException_when_parameterIsNull() { - assertThatThrownBy(() -> thumbnailUtil.saveThumbnail(null, null)) - .isInstanceOf(NullPointerException.class); - assertThatThrownBy(() -> thumbnailUtil.saveThumbnail(validMultipartFile, null)) - .isInstanceOf(NullPointerException.class); - assertThatThrownBy(() -> thumbnailUtil.deleteFileAndEntity(null)) - .isInstanceOf(NullPointerException.class); - } +// @Test +// @DisplayName("썸네일 저장 시 type이 null이면 NullPointerException을 발생시킨다.") +// void should_throwNullPointerException_when_parameterIsNull() { +// assertThatThrownBy(() -> thumbnailUtil.saveThumbnail(null, null)) +// .isInstanceOf(NullPointerException.class); +// assertThatThrownBy(() -> thumbnailUtil.saveThumbnail(validMultipartFile, null)) +// .isInstanceOf(NullPointerException.class); +// assertThatThrownBy(() -> thumbnailUtil.deleteFileAndEntity(null)) +// .isInstanceOf(NullPointerException.class); +// } private void deleteTestFile(Thumbnail thumbnail) { File thumbnailFile = new File(ROOT_PATH + separator + thumbnail.getPath());