From 3aa6bfa5d8b0b06247950109a441a13e0b588361 Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 14 Aug 2023 17:03:12 +0900 Subject: [PATCH] =?UTF-8?q?=EB=9F=AC=EB=84=88=EA=B0=80=20=EC=84=9C?= =?UTF-8?q?=ED=8F=AC=ED=84=B0=20=EB=AA=A9=EB=A1=9D=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=84=9C=ED=8F=AC=ED=84=B0=EB=A5=BC=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=ED=95=98=EB=8A=94=20API=20=EA=B5=AC=ED=98=84=20(#346)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 지원한 서포터를 선택하는 API 구현 * test: RunnerPostService 테스트 추가 * refactor: 파라미터 명 수정 * test: SupporterRunnerPostRepositoryReadTest 추가 * refactor: 잘못된 redirect uri 변경 * test: Restdocs 테스트 추가 * test: isNotOwner 테스트 추가 * style: startReview 메서드 줄 변경 * refactor: Objects.equals 대신 도메인 로직을 사용하도록 변경 * refactor: 메서드 이름 변경 * refactor: 병합 출돌 해결 * refactor: 필요없는 메서드 삭제 * docs: index.adoc에 서포터 선택 API 추가 및 depth 조절 * refactor: 사용하지 않는 메서드 제거 * test: 컨벤션 맞게 반영 * test: DeadlineFixture 로 변경 * refactor: 예외 메세지 이름 변경 * refactor: private 메서드 병합 * test: notSavedId given 절로 변경 * refactor: hasMessage 제거 및 DTO 이름 변경 --- .../docs/asciidoc/RunnerPostUpdateApi.adoc | 24 +++++ .../docs/asciidoc/RunnerProfileReadApi.adoc | 27 ++---- backend/baton/src/docs/asciidoc/index.adoc | 3 + .../common/exception/ClientErrorCode.java | 1 + .../controller/RunnerPostController.java | 16 +++- .../runnerpost/service/RunnerPostService.java | 27 +++++- .../service/dto/RunnerPostUpdateRequest.java | 34 ++++--- .../SupporterRunnerPostRepository.java | 3 +- .../baton/assure/common/AssuredSupport.java | 24 ++++- .../runnerpost/RunnerPostAssuredSupport.java | 21 +++++ .../RunnerPostAssuredUpdateTest.java | 55 +++++++++++ .../touch/baton/config/AssuredTestConfig.java | 6 +- .../read/RunnerPostReadApiTest.java | 5 +- .../update/RunnerPostUpdateApiTest.java | 74 +++++++++++++++ .../domain/runnerpost/RunnerPostTest.java | 25 +++++ ...SupporterRunnerPostRepositoryReadTest.java | 67 +++++++++++++ .../service/RunnerPostServiceUpdateTest.java | 93 ++++++++++++++++++- 17 files changed, 464 insertions(+), 41 deletions(-) create mode 100644 backend/baton/src/docs/asciidoc/RunnerPostUpdateApi.adoc create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredUpdateTest.java create mode 100644 backend/baton/src/test/java/touch/baton/document/runnerpost/update/RunnerPostUpdateApiTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/SupporterRunnerPostRepositoryReadTest.java diff --git a/backend/baton/src/docs/asciidoc/RunnerPostUpdateApi.adoc b/backend/baton/src/docs/asciidoc/RunnerPostUpdateApi.adoc new file mode 100644 index 000000000..9f16e347e --- /dev/null +++ b/backend/baton/src/docs/asciidoc/RunnerPostUpdateApi.adoc @@ -0,0 +1,24 @@ +ifndef::snippets[] +:snippets: ../../../build/generated-snippets +endif::[] +:doctype: book +:icons: font +:source-highlighter: highlight.js +:toc: left +:toclevels: 3 +:sectlinks: +:operation-http-request-title: Example Request +:operation-http-response-title: Example Response + +=== *러너 게시글 수정* + +==== *리뷰할 서포터 선택 API* + +===== *Http Request* +include::{snippets}/../../build/generated-snippets/runner-post-update-api-test/update-runner-post-supporter/http-request.adoc[] + +===== *Http Request Header* +include::{snippets}/../../build/generated-snippets/runner-post-update-api-test/update-runner-post-supporter/request-headers.adoc[] + +===== *Http Response* +include::{snippets}/../../build/generated-snippets/runner-post-update-api-test/update-runner-post-supporter/http-response.adoc[] diff --git a/backend/baton/src/docs/asciidoc/RunnerProfileReadApi.adoc b/backend/baton/src/docs/asciidoc/RunnerProfileReadApi.adoc index 98e6b5680..a3cf6c1b9 100644 --- a/backend/baton/src/docs/asciidoc/RunnerProfileReadApi.adoc +++ b/backend/baton/src/docs/asciidoc/RunnerProfileReadApi.adoc @@ -10,36 +10,29 @@ endif::[] :operation-http-request-title: Example Request :operation-http-response-title: Example Response -== *러너 프로필 조회* +=== *러너 프로필 조회* -=== *러너 프로필 조회 API* - -==== *Http Request* +==== *러너 프로필 조회 API* +===== *Http Request* include::{snippets}/../../build/generated-snippets/runner-profile-read-api-test/read-runner-profile/http-request.adoc[] -==== *Http Response* - +===== *Http Response* include::{snippets}/../../build/generated-snippets/runner-profile-read-api-test/read-runner-profile/http-response.adoc[] -==== *Http Response Fields* - +===== *Http Response Fields* include::{snippets}/../../build/generated-snippets/runner-profile-read-api-test/read-runner-profile/response-fields.adoc[] -=== *러너 마이페이지 프로필 조회 API* - -==== *Http Request* +==== *러너 마이페이지 프로필 조회 API* +===== *Http Request* include::{snippets}/../../build/generated-snippets/runner-profile-read-api-test/read-my-profile-by-token/http-request.adoc[] -==== *Http Request Headers* - +===== *Http Request Headers* include::{snippets}/../../build/generated-snippets/runner-profile-read-api-test/read-my-profile-by-token/request-headers.adoc[] -==== *Http Response* - +===== *Http Response* include::{snippets}/../../build/generated-snippets/runner-profile-read-api-test/read-my-profile-by-token/http-response.adoc[] -==== *Http Response Fields* - +===== *Http Response Fields* include::{snippets}/../../build/generated-snippets/runner-profile-read-api-test/read-my-profile-by-token/response-fields.adoc[] diff --git a/backend/baton/src/docs/asciidoc/index.adoc b/backend/baton/src/docs/asciidoc/index.adoc index 17e362705..8124a19c4 100644 --- a/backend/baton/src/docs/asciidoc/index.adoc +++ b/backend/baton/src/docs/asciidoc/index.adoc @@ -14,6 +14,9 @@ endif::[] include::MemberLoginProfileReadApi.adoc[] +== *[ 게시글 ]* +include::RunnerPostUpdateApi.adoc[] + == *[ 러너 ]* include::RunnerProfileReadApi.adoc[] diff --git a/backend/baton/src/main/java/touch/baton/domain/common/exception/ClientErrorCode.java b/backend/baton/src/main/java/touch/baton/domain/common/exception/ClientErrorCode.java index 9a26e510a..320086a75 100644 --- a/backend/baton/src/main/java/touch/baton/domain/common/exception/ClientErrorCode.java +++ b/backend/baton/src/main/java/touch/baton/domain/common/exception/ClientErrorCode.java @@ -12,6 +12,7 @@ public enum ClientErrorCode { PAST_DEADLINE(HttpStatus.BAD_REQUEST, "RP006", "마감일은 오늘보다 과거일 수 없습니다."), RUNNER_POST_NOT_FOUND(HttpStatus.NOT_FOUND, "RP007", "존재하지 않는 게시물입니다."), TAGS_ARE_NULL(HttpStatus.BAD_REQUEST, "RP008", "태그 목록을 빈 값이라도 입력해주세요."), + ASSIGN_SUPPORTER_ID_IS_NULL(HttpStatus.BAD_REQUEST, "RP009", "선택한 서포터의 식별자를 입력해주세요."), REVIEW_TYPE_IS_NULL(HttpStatus.BAD_REQUEST, "FB001", "만족도를 입력해주세요."), SUPPORTER_ID_IS_NULL(HttpStatus.BAD_REQUEST, "FB002", "서포터 식별자를 입력해주세요."), diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/RunnerPostController.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/RunnerPostController.java index c4142827d..7ba772fdb 100644 --- a/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/RunnerPostController.java +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/RunnerPostController.java @@ -111,7 +111,7 @@ public ResponseEntity deleteByRunnerPostId(@AuthRunnerPrincipal final Runn @PutMapping("/{runnerPostId}") public ResponseEntity update(@AuthRunnerPrincipal final Runner runner, @PathVariable final Long runnerPostId, - @Valid @RequestBody final RunnerPostUpdateRequest request + @Valid @RequestBody final RunnerPostUpdateRequest.Default request ) { final Long updatedId = runnerPostService.updateRunnerPost(runnerPostId, runner, request); final URI redirectUri = UriComponentsBuilder.fromPath("/api/v1/posts/runner") @@ -181,4 +181,18 @@ public ResponseEntity updateSupporterCancelRunnerPost(@AuthSupporterPrinci .toUri(); return ResponseEntity.noContent().location(redirectUri).build(); } + + @PatchMapping("/{runnerPostId}/supporters") + public ResponseEntity updateRunnerPostAppliedSupporter(@AuthRunnerPrincipal final Runner runner, + @PathVariable final Long runnerPostId, + @Valid @RequestBody final RunnerPostUpdateRequest.SelectSupporter request + ) { + runnerPostService.updateRunnerPostAppliedSupporter(runner, runnerPostId, request); + + final URI redirectUri = UriComponentsBuilder.fromPath("/api/v1/posts/runner") + .path("/{runnerPostId}") + .buildAndExpand(runnerPostId) + .toUri(); + return ResponseEntity.noContent().location(redirectUri).build(); + } } diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/RunnerPostService.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/RunnerPostService.java index bbd3634bc..6d63d4131 100644 --- a/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/RunnerPostService.java +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/RunnerPostService.java @@ -143,7 +143,7 @@ public void deleteByRunnerPostId(final Long runnerPostId, final Runner runner) { } @Transactional - public Long updateRunnerPost(final Long runnerPostId, final Runner runner, final RunnerPostUpdateRequest request) { + public Long updateRunnerPost(final Long runnerPostId, final Runner runner, final RunnerPostUpdateRequest.Default request) { // TODO: 메소드 분리 // FIXME: 2023/08/03 주인 확인 로직 넣기 final RunnerPost runnerPost = runnerPostRepository.findById(runnerPostId) @@ -221,4 +221,29 @@ public void deleteSupporterRunnerPost(final Supporter supporter, final Long runn } supporterRunnerPostRepository.deleteBySupporterIdAndRunnerPostId(supporter.getId(), runnerPostId); } + + @Transactional + public void updateRunnerPostAppliedSupporter(final Runner runner, + final Long runnerPostId, + final RunnerPostUpdateRequest.SelectSupporter request + ) { + final Supporter foundApplySupporter = supporterRepository.findById(request.supporterId()) + .orElseThrow(() -> new RunnerPostBusinessException("해당하는 식별자값의 서포터를 찾을 수 없습니다.")); + final RunnerPost foundRunnerPost = runnerPostRepository.findById(runnerPostId) + .orElseThrow(() -> new RunnerPostBusinessException("RunnerPost 의 식별자값으로 러너 게시글을 조회할 수 없습니다.")); + + if (isApplySupporter(runnerPostId, foundApplySupporter)) { + throw new RunnerPostBusinessException("게시글에 리뷰를 제안한 서포터가 아닙니다."); + } + if (foundRunnerPost.isNotOwner(runner)) { + throw new RunnerPostBusinessException("RunnerPost 의 글쓴이와 다른 사용자입니다."); + } + + foundRunnerPost.assignSupporter(foundApplySupporter); + } + + private boolean isApplySupporter(final Long runnerPostId, final Supporter foundSupporter) { + return !supporterRunnerPostRepository.existsByRunnerPostIdAndSupporterId(runnerPostId, foundSupporter.getId()); + } + } diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/dto/RunnerPostUpdateRequest.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/dto/RunnerPostUpdateRequest.java index 9bfd636d2..5a17a8a99 100644 --- a/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/dto/RunnerPostUpdateRequest.java +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/dto/RunnerPostUpdateRequest.java @@ -8,17 +8,25 @@ import java.time.LocalDateTime; import java.util.List; -public record RunnerPostUpdateRequest(@ValidNotNull(clientErrorCode = ClientErrorCode.TITLE_IS_NULL) - String title, - @ValidNotNull(clientErrorCode = ClientErrorCode.TAGS_ARE_NULL) - List tags, - @ValidNotNull(clientErrorCode = ClientErrorCode.PULL_REQUEST_URL_IS_NULL) - String pullRequestUrl, - @ValidNotNull(clientErrorCode = ClientErrorCode.DEADLINE_IS_NULL) - @ValidFuture(clientErrorCode = ClientErrorCode.PAST_DEADLINE) - LocalDateTime deadline, - @ValidNotNull(clientErrorCode = ClientErrorCode.CONTENTS_ARE_NULL) - @ValidMaxLength(clientErrorCode = ClientErrorCode.CONTENTS_OVERFLOW, max = 1000) - String contents -) { +public record RunnerPostUpdateRequest() { + + public record Default(@ValidNotNull(clientErrorCode = ClientErrorCode.TITLE_IS_NULL) + String title, + @ValidNotNull(clientErrorCode = ClientErrorCode.TAGS_ARE_NULL) + List tags, + @ValidNotNull(clientErrorCode = ClientErrorCode.PULL_REQUEST_URL_IS_NULL) + String pullRequestUrl, + @ValidNotNull(clientErrorCode = ClientErrorCode.DEADLINE_IS_NULL) + @ValidFuture(clientErrorCode = ClientErrorCode.PAST_DEADLINE) + LocalDateTime deadline, + @ValidNotNull(clientErrorCode = ClientErrorCode.CONTENTS_ARE_NULL) + @ValidMaxLength(clientErrorCode = ClientErrorCode.CONTENTS_OVERFLOW, max = 1000) + String contents + ) { + } + + public record SelectSupporter(@ValidNotNull(clientErrorCode = ClientErrorCode.ASSIGN_SUPPORTER_ID_IS_NULL) + Long supporterId + ) { + } } diff --git a/backend/baton/src/main/java/touch/baton/domain/supporter/repository/SupporterRunnerPostRepository.java b/backend/baton/src/main/java/touch/baton/domain/supporter/repository/SupporterRunnerPostRepository.java index 995bdcbba..b829b10ad 100644 --- a/backend/baton/src/main/java/touch/baton/domain/supporter/repository/SupporterRunnerPostRepository.java +++ b/backend/baton/src/main/java/touch/baton/domain/supporter/repository/SupporterRunnerPostRepository.java @@ -18,5 +18,6 @@ having srp.runnerPost.id in (:runnerPostIds) List countByRunnerPostIdIn(@Param("runnerPostIds") final List runnerPostIds); void deleteBySupporterIdAndRunnerPostId(final Long supporterId, final Long runnerPostId); -} + boolean existsByRunnerPostIdAndSupporterId(final Long runnerPostId, final Long supporterId); +} diff --git a/backend/baton/src/test/java/touch/baton/assure/common/AssuredSupport.java b/backend/baton/src/test/java/touch/baton/assure/common/AssuredSupport.java index 83078e856..da75e0122 100644 --- a/backend/baton/src/test/java/touch/baton/assure/common/AssuredSupport.java +++ b/backend/baton/src/test/java/touch/baton/assure/common/AssuredSupport.java @@ -57,6 +57,17 @@ public static ExtractableResponse get(final String uri, final String a } + public static ExtractableResponse get(final String uri, final Map queryParams) { + return RestAssured + .given().log().ifValidationFails() + .contentType(APPLICATION_JSON_VALUE) + .queryParams(queryParams) + .when().log().ifValidationFails() + .get(uri) + .then().log().ifError() + .extract(); + } + public static ExtractableResponse patch(final String uri, final String accessToken, final Object params) { return RestAssured .given().log().ifValidationFails() @@ -69,13 +80,20 @@ public static ExtractableResponse patch(final String uri, final String .extract(); } - public static ExtractableResponse get(final String uri, final Map queryParams) { + public static ExtractableResponse patch(final String uri, + final String pathParamName, + final Long id, + final Object requestBody, + final String accessToken + ) { return RestAssured .given().log().ifValidationFails() + .auth().preemptive().oauth2(accessToken) .contentType(APPLICATION_JSON_VALUE) - .queryParams(queryParams) + .pathParam(pathParamName, id) + .body(requestBody) .when().log().ifValidationFails() - .get(uri) + .patch(uri) .then().log().ifError() .extract(); } diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredSupport.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredSupport.java index 462d34484..ea82e1c22 100644 --- a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredSupport.java +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredSupport.java @@ -8,8 +8,10 @@ import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import touch.baton.assure.common.AssuredSupport; +import touch.baton.assure.common.HttpStatusAndLocationHeader; import touch.baton.domain.common.response.PageResponse; import touch.baton.domain.runnerpost.controller.response.RunnerPostResponse; +import touch.baton.domain.runnerpost.service.dto.RunnerPostUpdateRequest; import touch.baton.domain.runnerpost.vo.ReviewStatus; import java.util.List; @@ -17,6 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.springframework.http.HttpHeaders.LOCATION; @SuppressWarnings("NonAsciiCharacters") public class RunnerPostAssuredSupport { @@ -73,6 +76,17 @@ public static class RunnerPostClientRequestBuilder { return this; } + public RunnerPostClientRequestBuilder 러너가_서포터를_선택한다(final Long 게시글_식별자값, + final RunnerPostUpdateRequest.SelectSupporter 서포터_선택_요청_정보 + ) { + response = AssuredSupport.patch("/api/v1/posts/runner/{runnerPostId}/supporters", + "runnerPostId", 게시글_식별자값, + 서포터_선택_요청_정보, + accessToken + ); + return this; + } + public RunnerPostServerResponseBuilder 서버_응답() { return new RunnerPostServerResponseBuilder(response); } @@ -119,5 +133,12 @@ public RunnerPostServerResponseBuilder(final ExtractableResponse respo assertThat(response.statusCode()) .isEqualTo(HTTP_STATUS.value()); } + + public void 러너_게시글에_서포터가_성공적으로_선택되었는지_확인한다(final HttpStatusAndLocationHeader httpStatusAndLocationHeader) { + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(httpStatusAndLocationHeader.getHttpStatus().value()); + softly.assertThat(response.header(LOCATION)).contains(httpStatusAndLocationHeader.getLocation()); + }); + } } } diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredUpdateTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredUpdateTest.java new file mode 100644 index 000000000..cd3abfb49 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredUpdateTest.java @@ -0,0 +1,55 @@ +package touch.baton.assure.runnerpost; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import touch.baton.assure.common.HttpStatusAndLocationHeader; +import touch.baton.config.AssuredTestConfig; +import touch.baton.domain.member.Member; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.service.dto.RunnerPostUpdateRequest; +import touch.baton.domain.supporter.Supporter; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.RunnerFixture; +import touch.baton.fixture.domain.RunnerPostFixture; +import touch.baton.fixture.domain.SupporterFixture; +import touch.baton.fixture.domain.SupporterRunnerPostFixture; + +import java.time.LocalDateTime; + +import static touch.baton.fixture.vo.DeadlineFixture.deadline; + +@SuppressWarnings("NonAsciiCharacters") +public class RunnerPostAssuredUpdateTest extends AssuredTestConfig { + + @Test + void 러너가_서포터_목록에서_서포터를_선택할_수_있다() { + // given + final String 디투_소셜_아이디 = "hongSile"; + final Member 사용자_디투 = memberRepository.save(MemberFixture.createWithSocialId(디투_소셜_아이디)); + final Runner 러너_디투 = runnerRepository.save(RunnerFixture.createRunner(사용자_디투)); + final String 디투_토큰 = login(디투_소셜_아이디); + + final RunnerPost 디투_게시글 = runnerPostRepository.save(RunnerPostFixture.create(러너_디투, deadline(LocalDateTime.now().plusDays(10)))); + + final Member 사용자_에단 = memberRepository.save(MemberFixture.createEthan()); + final Supporter 서포터_에단 = supporterRepository.save(SupporterFixture.create(사용자_에단)); + + 서포터가_리뷰_게시글에_리뷰_제안을_한다(디투_게시글, 서포터_에단); + + final RunnerPostUpdateRequest.SelectSupporter 서포터_선택_요청_정보 = new RunnerPostUpdateRequest.SelectSupporter(서포터_에단.getId()); + + // when, then + RunnerPostAssuredSupport + .클라이언트_요청() + .토큰으로_로그인한다(디투_토큰) + .러너가_서포터를_선택한다(디투_게시글.getId(), 서포터_선택_요청_정보) + + .서버_응답() + .러너_게시글에_서포터가_성공적으로_선택되었는지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); + } + + private void 서포터가_리뷰_게시글에_리뷰_제안을_한다(final RunnerPost 지원할_게시글, final Supporter 지원한_서포터) { + supporterRunnerPostRepository.save(SupporterRunnerPostFixture.create(지원할_게시글, 지원한_서포터)); + } +} diff --git a/backend/baton/src/test/java/touch/baton/config/AssuredTestConfig.java b/backend/baton/src/test/java/touch/baton/config/AssuredTestConfig.java index eaae79ec9..3bd2e8b24 100644 --- a/backend/baton/src/test/java/touch/baton/config/AssuredTestConfig.java +++ b/backend/baton/src/test/java/touch/baton/config/AssuredTestConfig.java @@ -46,12 +46,12 @@ public abstract class AssuredTestConfig { @Autowired protected SupporterRunnerPostRepository supporterRunnerPostRepository; - @MockBean - private JwtDecoder jwtDecoder; - @Autowired protected TechnicalTagRepository technicalTagRepository; + @MockBean + private JwtDecoder jwtDecoder; + @BeforeEach void assuredTestSetUp(@LocalServerPort int port) { RestAssured.port = port; diff --git a/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadApiTest.java b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadApiTest.java index 2b59cf182..e8b0246ec 100644 --- a/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadApiTest.java +++ b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadApiTest.java @@ -33,7 +33,10 @@ import static org.mockito.Mockito.spy; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.payload.JsonFieldType.*; +import static org.springframework.restdocs.payload.JsonFieldType.ARRAY; +import static org.springframework.restdocs.payload.JsonFieldType.BOOLEAN; +import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; diff --git a/backend/baton/src/test/java/touch/baton/document/runnerpost/update/RunnerPostUpdateApiTest.java b/backend/baton/src/test/java/touch/baton/document/runnerpost/update/RunnerPostUpdateApiTest.java new file mode 100644 index 000000000..1f250fef8 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/document/runnerpost/update/RunnerPostUpdateApiTest.java @@ -0,0 +1,74 @@ +package touch.baton.document.runnerpost.update; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import touch.baton.config.RestdocsConfig; +import touch.baton.domain.member.Member; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runnerpost.controller.RunnerPostController; +import touch.baton.domain.runnerpost.service.RunnerPostService; +import touch.baton.domain.runnerpost.service.dto.RunnerPostUpdateRequest; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.RunnerFixture; + +import java.util.Optional; + +import static org.apache.http.HttpHeaders.AUTHORIZATION; +import static org.apache.http.HttpHeaders.CONTENT_TYPE; +import static org.apache.http.HttpHeaders.LOCATION; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.any; +import static org.mockito.BDDMockito.when; +import static org.mockito.BDDMockito.willDoNothing; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(RunnerPostController.class) +public class RunnerPostUpdateApiTest extends RestdocsConfig { + + @MockBean + private RunnerPostService runnerPostService; + + @BeforeEach + void setUp() { + restdocsSetUp(new RunnerPostController(runnerPostService)); + } + + @DisplayName("제안한 서포터 목록 중에서 서포터로 선택하는 API") + @Test + void updateRunnerPostSupporter() throws Exception { + // given + final String ditooSocialId = "helloToken"; + final String token = getAccessTokenBySocialId(ditooSocialId); + final Member ditooMember = MemberFixture.createWithSocialId(ditooSocialId); + final Runner ditooRunner = RunnerFixture.createRunner(ditooMember); + + final RunnerPostUpdateRequest.SelectSupporter request = new RunnerPostUpdateRequest.SelectSupporter(1L); + + // when + willDoNothing().given(runnerPostService).updateRunnerPostAppliedSupporter(any(Runner.class), anyLong(), any(RunnerPostUpdateRequest.SelectSupporter.class)); + when(oauthRunnerRepository.joinByMemberSocialId(any())).thenReturn(Optional.ofNullable(ditooRunner)); + + // then + mockMvc.perform(patch("/api/v1/posts/runner/{runnerPostId}/supporters", 1L) + .header(AUTHORIZATION, "Bearer " + token) + .contentType(APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNoContent()) + .andExpect(redirectedUrl("/api/v1/posts/runner/1")) + .andDo(restDocs.document( + requestHeaders(headerWithName(AUTHORIZATION).description("Bearer JWT"), + headerWithName(CONTENT_TYPE).description(APPLICATION_JSON_VALUE)), + responseHeaders(headerWithName(LOCATION).description("Redirect URI")) + )).andDo(print()); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/RunnerPostTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/RunnerPostTest.java index 26972c45f..2404798d5 100644 --- a/backend/baton/src/test/java/touch/baton/domain/runnerpost/RunnerPostTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/RunnerPostTest.java @@ -1,5 +1,6 @@ package touch.baton.domain.runnerpost; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -27,7 +28,11 @@ import touch.baton.domain.tag.RunnerPostTags; import touch.baton.domain.tag.Tag; import touch.baton.domain.technicaltag.SupporterTechnicalTags; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.RunnerFixture; +import touch.baton.fixture.domain.RunnerPostFixture; import touch.baton.fixture.domain.RunnerTechnicalTagsFixture; +import touch.baton.fixture.vo.DeadlineFixture; import java.time.LocalDateTime; import java.util.ArrayList; @@ -489,4 +494,24 @@ private static Stream reviewStatusDummy() { } } + + // FIXME: 2023/08/13 아이디 없어서 테스트가 통과 안되는데 어떻게 함? + @Disabled + @DisplayName("글 주인이 아니면 true 를 반환한다.") + @Test + void isNotOwner() { + // given + final Member ethanMember = MemberFixture.createEthan(); + final Runner ownerRunner = RunnerFixture.createRunner(ethanMember); + final RunnerPost runnerPost = RunnerPostFixture.create(ownerRunner, DeadlineFixture.deadline(LocalDateTime.now().plusHours(10))); + + final Member hyenaMember = MemberFixture.createHyena(); + final Runner notOwnerRunner = RunnerFixture.createRunner(hyenaMember); + + // when, then + assertAll( + () -> assertThat(runnerPost.isNotOwner(notOwnerRunner)).isTrue(), + () -> assertThat(runnerPost.isNotOwner(ownerRunner)).isFalse() + ); + } } diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/SupporterRunnerPostRepositoryReadTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/SupporterRunnerPostRepositoryReadTest.java new file mode 100644 index 000000000..4ee839fe4 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/SupporterRunnerPostRepositoryReadTest.java @@ -0,0 +1,67 @@ +package touch.baton.domain.runnerpost.repository; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import touch.baton.config.RepositoryTestConfig; +import touch.baton.domain.member.Member; +import touch.baton.domain.member.repository.MemberRepository; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runner.repository.RunnerRepository; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.supporter.repository.SupporterRepository; +import touch.baton.domain.supporter.repository.SupporterRunnerPostRepository; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.RunnerFixture; +import touch.baton.fixture.domain.RunnerPostFixture; +import touch.baton.fixture.domain.SupporterFixture; +import touch.baton.fixture.domain.SupporterRunnerPostFixture; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static touch.baton.fixture.vo.DeadlineFixture.deadline; + +class SupporterRunnerPostRepositoryReadTest extends RepositoryTestConfig { + + @Autowired + private SupporterRunnerPostRepository supporterRunnerPostRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private RunnerRepository runnerRepository; + + @Autowired + private SupporterRepository supporterRepository; + + @Autowired + private RunnerPostRepository runnerPostRepository; + + @DisplayName("RunnerPostId 와 SupporterId 로 존재 유무를 확인할 수 있다.") + @Test + void existsByRunnerPostIdAndSupporterId() { + // given + final Member ehtanMember = memberRepository.save(MemberFixture.createEthan()); + final Runner runnerPostOwner = runnerRepository.save(RunnerFixture.createRunner(ehtanMember)); + final RunnerPost runner = runnerPostRepository.save(RunnerPostFixture.create(runnerPostOwner, + deadline(LocalDateTime.now().plusDays(10)))); + + final Member hyenaMember = memberRepository.save(MemberFixture.createHyena()); + final Supporter supporter = supporterRepository.save(SupporterFixture.create(hyenaMember)); + supporterRunnerPostRepository.save(SupporterRunnerPostFixture.create(runner, supporter)); + + final Long notSavedRunnerPostId = -1L; + final Long notSavedSupporter = -1L; + + // when, then + assertSoftly(softly -> { + softly.assertThat(supporterRunnerPostRepository.existsByRunnerPostIdAndSupporterId(runner.getId(), supporter.getId())).isTrue(); + softly.assertThat(supporterRunnerPostRepository.existsByRunnerPostIdAndSupporterId(notSavedRunnerPostId, supporter.getId())).isFalse(); + softly.assertThat(supporterRunnerPostRepository.existsByRunnerPostIdAndSupporterId(runner.getId(), notSavedSupporter)).isFalse(); + } + ); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceUpdateTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceUpdateTest.java index 2a5a375ee..21222d384 100644 --- a/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceUpdateTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceUpdateTest.java @@ -9,14 +9,19 @@ import touch.baton.domain.member.Member; import touch.baton.domain.runner.Runner; import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.exception.RunnerPostBusinessException; import touch.baton.domain.runnerpost.service.dto.RunnerPostUpdateRequest; import touch.baton.domain.runnerpost.vo.Deadline; import touch.baton.domain.runnerpost.vo.PullRequestUrl; +import touch.baton.domain.runnerpost.vo.ReviewStatus; +import touch.baton.domain.supporter.Supporter; import touch.baton.domain.tag.RunnerPostTag; import touch.baton.fixture.domain.MemberFixture; import touch.baton.fixture.domain.RunnerFixture; import touch.baton.fixture.domain.RunnerPostFixture; import touch.baton.fixture.domain.RunnerPostTagsFixture; +import touch.baton.fixture.domain.SupporterFixture; +import touch.baton.fixture.domain.SupporterRunnerPostFixture; import java.time.LocalDateTime; import java.util.ArrayList; @@ -24,6 +29,7 @@ import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; import static touch.baton.domain.runnerpost.vo.ReviewStatus.NOT_STARTED; import static touch.baton.fixture.vo.ContentsFixture.contents; @@ -41,6 +47,10 @@ class RunnerPostServiceUpdateTest extends ServiceTestConfig { private static final LocalDateTime DEADLINE = LocalDateTime.now().plusHours(100); private static final String CONTENTS = "싸게 부탁드려요."; + private static Runner runnerPostOwner; + private static RunnerPost targetRunnerPost; + private static Supporter applySupporter; + private RunnerPostService runnerPostService; @BeforeEach @@ -52,13 +62,22 @@ void setUp() { supporterRepository, supporterRunnerPostRepository ); + + final Member ehtanMember = memberRepository.save(MemberFixture.createEthan()); + runnerPostOwner = runnerRepository.save(RunnerFixture.createRunner(ehtanMember)); + targetRunnerPost = runnerPostRepository.save(RunnerPostFixture.create(runnerPostOwner, + deadline(LocalDateTime.now().plusDays(10)))); + + final Member hyenaMember = memberRepository.save(MemberFixture.createHyena()); + applySupporter = supporterRepository.save(SupporterFixture.create(hyenaMember)); + supporterRunnerPostRepository.save(SupporterRunnerPostFixture.create(targetRunnerPost, applySupporter)); } @DisplayName("Runner Post 수정에 성공한다.") @Test void success() { // given - final RunnerPostUpdateRequest request = new RunnerPostUpdateRequest( + final RunnerPostUpdateRequest.Default request = new RunnerPostUpdateRequest.Default( TITLE, List.of(TAG, OTHER_TAG), PULL_REQUEST_URL, DEADLINE, CONTENTS); final Member ditoo = MemberFixture.createDitoo(); memberRepository.save(ditoo); @@ -97,4 +116,76 @@ void success() { .toList() ).containsExactly(TAG, OTHER_TAG); } + + @DisplayName("러너는 자신의 글에 제안한 서포터를 서포터로 선택할 수 있다.") + @Test + void updateRunnerPostAppliedSupporter() { + // given + final RunnerPostUpdateRequest.SelectSupporter request = new RunnerPostUpdateRequest.SelectSupporter(applySupporter.getId()); + + // when + runnerPostService.updateRunnerPostAppliedSupporter(runnerPostOwner, targetRunnerPost.getId(), request); + + // then + final Optional maybeRunnerPost = runnerPostRepository.findById(targetRunnerPost.getId()); + assertThat(maybeRunnerPost).isPresent(); + + final RunnerPost actualRunnerPost = maybeRunnerPost.get(); + assertAll( + () -> assertThat(actualRunnerPost.getSupporter().getId()).isEqualTo(applySupporter.getId()), + () -> assertThat(actualRunnerPost.getReviewStatus()).isEqualTo(ReviewStatus.IN_PROGRESS) + ); + } + + @DisplayName("러너는 가입되어 있지 않는 서포터를 선택할 수 없다.") + @Test + void fail_updateRunnerPostAppliedSupporter_if_not_join_supporter() { + // given + final Long notJoinSupporterId = 1000000L; + final RunnerPostUpdateRequest.SelectSupporter request = new RunnerPostUpdateRequest.SelectSupporter(notJoinSupporterId); + + // when, then + assertThatThrownBy(() -> runnerPostService.updateRunnerPostAppliedSupporter(runnerPostOwner, targetRunnerPost.getId(), request)) + .isInstanceOf(RunnerPostBusinessException.class); + } + + @DisplayName("러너는 자신의 글에 제안한 서포터가 아니면 서포터로 선택할 수 없다.") + @Test + void fail_updateRunnerPostAppliedSupporter_if_not_apply_supporter() { + // given + final Member ditooMember = memberRepository.save(MemberFixture.createDitoo()); + final Supporter notApplySupporter = supporterRepository.save(SupporterFixture.create(ditooMember)); + + final RunnerPostUpdateRequest.SelectSupporter request = new RunnerPostUpdateRequest.SelectSupporter(notApplySupporter.getId()); + + // when, then + assertThatThrownBy(() -> runnerPostService.updateRunnerPostAppliedSupporter(runnerPostOwner, targetRunnerPost.getId(), request)) + .isInstanceOf(RunnerPostBusinessException.class); + } + + @DisplayName("러너는 작성된 글이 아니면 서포터를 선택할 수 없다.") + @Test + void fail_updateRunnerPostAppliedSupporter_if_is_not_written_runnerPost() { + // given + final RunnerPostUpdateRequest.SelectSupporter request = new RunnerPostUpdateRequest.SelectSupporter(applySupporter.getId()); + final Long notWrittenRunnerPostId = 1000000L; + + // when, then + assertThatThrownBy(() -> runnerPostService.updateRunnerPostAppliedSupporter(runnerPostOwner, notWrittenRunnerPostId, request)) + .isInstanceOf(RunnerPostBusinessException.class); + } + + @DisplayName("러너는 자신의 글이 아니면 서포터를 선택할 수 없다.") + @Test + void fail_updateRunnerPostAppliedSupporter_if_is_not_owner_of_runnerPost() { + // given + final Member ditooMember = memberRepository.save(MemberFixture.createDitoo()); + final Runner notOwnerRunner = runnerRepository.save(RunnerFixture.createRunner(ditooMember)); + + final RunnerPostUpdateRequest.SelectSupporter request = new RunnerPostUpdateRequest.SelectSupporter(applySupporter.getId()); + + // when, then + assertThatThrownBy(() -> runnerPostService.updateRunnerPostAppliedSupporter(notOwnerRunner, targetRunnerPost.getId(), request)) + .isInstanceOf(RunnerPostBusinessException.class); + } }