From d040704c60d198beebdc60f255745d7ba9a1c401 Mon Sep 17 00:00:00 2001 From: Ethan Date: Wed, 20 Sep 2023 19:42:41 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B0=B1=EC=97=94=EB=93=9C=20main=20=EB=B3=91?= =?UTF-8?q?=ED=95=A9=20v1.1=20(#578)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge dev/BE --- .github/CODEOWNERS | 5 + .github/workflows/dev-be-ci-cd-push.yml | 2 +- .../docs/asciidoc/GithubBranchCreateApi.adoc | 29 +++ .../src/docs/asciidoc/GithubOauthApi.adoc | 37 +++ .../src/docs/asciidoc/RefreshTokenApi.adoc | 37 +++ .../src/docs/asciidoc/RunnerPostReadApi.adoc | 44 +++- .../src/docs/asciidoc/RunnerReadApi.adoc | 26 ++ backend/baton/src/docs/asciidoc/index.adoc | 13 +- .../java/touch/baton/common/LoggerAspect.java | 64 +++-- .../domain/common/GlobalControllerAdvice.java | 19 ++ .../common/exception/ClientErrorCode.java | 15 +- .../baton/domain/common/vo/WatchedCount.java | 2 +- .../SupporterFeedbackRepository.java | 2 + .../feedback/service/FeedbackService.java | 6 +- .../controller/MemberBranchController.java | 32 +++ .../service/dto/GithubBranchManageable.java | 6 + .../service/dto/GithubRepoNameRequest.java | 7 + .../domain/oauth/AuthorizationHeader.java | 27 ++ .../baton/domain/oauth/OauthInformation.java | 2 +- .../AuthCodeRequestUrlProviderComposite.java | 2 + .../OauthInformationClientComposite.java | 2 + .../oauth/controller/OauthController.java | 57 ++++- .../UserPrincipalArgumentResolver.java | 8 +- .../repository/RefreshTokenRepository.java | 15 ++ .../domain/oauth/service/OauthService.java | 89 ++++++- .../baton/domain/oauth/token/AccessToken.java | 15 ++ .../baton/domain/oauth/token/ExpireDate.java | 40 +++ .../domain/oauth/token/RefreshToken.java | 77 ++++++ .../baton/domain/oauth/token/SocialToken.java | 15 ++ .../touch/baton/domain/oauth/token/Token.java | 31 +++ .../baton/domain/oauth/token/Tokens.java | 7 + .../RefreshTokenDomainException.java | 10 + .../baton/domain/runnerpost/RunnerPost.java | 101 ++++---- .../controller/RunnerPostController.java | 21 +- .../controller/RunnerPostReadController.java | 71 ++++++ .../response/RunnerPostResponse.java | 45 +--- .../repository/RunnerPostReadRepository.java | 54 ++++ .../repository/RunnerPostRepository.java | 4 +- .../repository/dto/ApplicantCountDto.java | 4 + .../dto/ApplicantCountMappingDto.java | 10 + .../service/RunnerPostReadService.java | 35 +++ .../runnerpost/service/RunnerPostService.java | 8 +- .../service/dto/RunnerPostCreateRequest.java | 19 +- .../service/dto/RunnerPostUpdateRequest.java | 20 -- .../domain/runnerpost/vo/CuriousContents.java | 32 +++ .../runnerpost/vo/ImplementedContents.java | 32 +++ .../domain/runnerpost/vo/IsReviewed.java | 35 +++ .../runnerpost/vo/PostscriptContents.java | 32 +++ .../touch/baton/domain/tag/RunnerPostTag.java | 4 - .../baton/domain/tag/RunnerPostTags.java | 2 + .../main/java/touch/baton/domain/tag/Tag.java | 24 +- .../domain/tag/controller/TagController.java | 36 +++ .../response/TagSearchResponse.java | 12 + .../response/TagSearchResponses.java | 12 + .../domain/tag/repository/TagRepository.java | 14 ++ .../baton/domain/tag/service/TagService.java | 24 ++ .../baton/domain/tag/vo/TagReducedName.java | 46 ++++ .../touch/baton/infra/auth/jwt/JwtConfig.java | 7 + .../baton/infra/auth/jwt/JwtDecoder.java | 35 ++- .../baton/infra/auth/jwt/JwtEncoder.java | 5 +- .../github/response/GithubMemberResponse.java | 2 +- .../baton/infra/exception/InfraException.java | 10 + .../infra/github/GithubBranchManager.java | 87 +++++++ .../github/request/CreateBranchRequest.java | 4 + .../response/ReadBranchInfoResponse.java | 4 + .../response/ReadLastCommitInfoResponse.java | 4 + .../baton/src/main/resources/application.yml | 12 +- .../main/resources/db/migration/V0__init.sql | 159 ++++++++++++ .../V20230910_1__add_new_contents_columns.sql | 3 + ...0_2__set_contents_data_to_new_contents.sql | 7 + .../V20230910_3__drop_column_of_contents.sql | 1 + ...230913__alter_tag_name_column_utf8_bin.sql | 2 + .../V20230914__create_refresh_token_table.sql | 15 ++ ...add_new_runner_post_is_reviewed_column.sql | 1 + ..._runner_post_watched_count_column_name.sql | 1 + .../baton/assure/common/AssuredSupport.java | 117 +++++---- .../baton/assure/common/JwtTestManager.java | 27 ++ .../assure/common/OauthLoginTestManager.java | 26 ++ .../touch/baton/assure/common/PathParams.java | 16 ++ .../baton/assure/common/QueryParams.java | 16 ++ .../SupporterFeedbackAssuredSupport.java | 16 +- .../SupporterFeedbackCreateAssuredTest.java | 104 +++++--- .../assure/member/MemberAssuredSupport.java | 6 +- .../member/MemberBranchAssuredSupport.java | 64 +++++ .../member/MemberBranchCreateAssuredTest.java | 27 ++ ...emberReadWithLoginedMemberAssuredTest.java | 20 +- .../assure/oauth/OauthAssuredSupport.java | 157 ++++++++++++ .../assure/oauth/OauthCreateAssuredTest.java | 27 ++ .../oauth/OauthRefreshTokenAssuredTest.java | 216 ++++++++++++++++ .../repository/TestMemberRepository.java | 17 ++ .../TestRefreshTokenRepository.java | 22 ++ .../TestRunnerPostReadRepository.java | 18 ++ .../repository/TestRunnerPostRepository.java | 13 + .../repository/TestRunnerRepository.java | 25 ++ .../repository/TestSupporterRepository.java | 25 ++ .../TestSupporterRunnerPostRepository.java | 10 + .../assure/repository/TestTagRepository.java | 6 + .../TestTechnicalTagRepository.java | 6 + .../assure/runner/RunnerAssuredSupport.java | 77 ++++-- .../RunnerReadByRunnerIdAssuredTest.java | 44 ++-- ...unnerReadWithLoginedRunnerAssuredTest.java | 25 +- .../runner/RunnerUpdateAssuredTest.java | 71 +++--- .../RunnerPostAssuredCreateSupport.java | 13 +- .../RunnerPostAssuredCreateTest.java | 51 ++-- .../runnerpost/RunnerPostAssuredSupport.java | 182 +++++++++++--- .../RunnerPostCreateAssuredTest.java | 129 +++++++--- .../RunnerPostDeleteAssuredTest.java | 154 ++++++++---- .../runnerpost/RunnerPostReadAssuredTest.java | 117 ++++++--- ...nnerPostReadByRunnerPostIdAssuredTest.java | 100 +++++--- .../RunnerPostReadWithLoginedAssuredTest.java | 122 ++++----- ...stReadWithLoginedSupporterAssuredTest.java | 206 ++++++++++------ ...SupporterIdAndReviewStatusAssuredTest.java | 128 +++++----- .../RunnerPostUpdateAssuredTest.java | 106 +++++--- .../supporter/SupporterAssuredSupport.java | 43 +++- ...SupporterReadBySupporterIdAssuredTest.java | 57 ++--- .../SupporterRunnerPostAssuredSupport.java | 21 +- .../SupporterRunnerPostDeleteAssuredTest.java | 80 +++--- .../supporter/SupporterUpdateAssuredTest.java | 70 +++--- .../baton/assure/tag/TagAssuredSupport.java | 85 +++++++ .../baton/assure/tag/TagReadAssuredTest.java | 58 +++++ .../ScheduleRunnerPostRepositoryTest.java | 13 +- .../touch/baton/config/AssuredTestConfig.java | 65 +++-- .../touch/baton/config/RestdocsConfig.java | 2 +- .../touch/baton/config/ServiceTestConfig.java | 4 + .../config/infra/auth/MockAuthTestConfig.java | 16 ++ .../config/infra/auth/jwt/MockJwtConfig.java | 34 +++ .../auth/oauth/MockRefreshTokenConfig.java | 35 +++ ...CodeRequestUrlProviderCompositeConfig.java | 22 ++ .../auth/oauth/authcode/MockAuthCodes.java | 20 ++ ...OauthInformationClientCompositeConfig.java | 52 ++++ .../github/MockGithubBranchServiceConfig.java | 21 ++ .../document/github/GithubBranchApiTest.java | 65 +++++ .../oauth/github/GithubOauthApiTest.java | 49 +++- .../oauth/token/RefreshTokenApiTest.java | 91 +++++++ .../create/RunnerPostCreateApiTest.java | 4 +- .../read/RunnerPostReadAllApiTest.java | 13 +- .../read/RunnerPostReadOneApiTest.java | 11 +- .../read/RunnerPostReadSearchApiTest.java | 125 ++++++++++ .../runnerpost/read/TagReadApiTest.java | 77 ++++++ .../baton/domain/common/vo/TagNameTest.java | 16 ++ .../SupporterFeedbackRepositoryTest.java | 65 +++++ .../feedback/service/FeedbackServiceTest.java | 36 ++- .../RefreshTokenRepositoryTest.java | 92 +++++++ .../oauth/service/OauthServiceUpdateTest.java | 184 ++++++++++++++ .../domain/oauth/token/RefreshTokenTest.java | 159 ++++++++++++ .../oauth/vo/AuthorizationHeaderTest.java | 61 +++++ .../domain/runnerpost/RunnerPostTest.java | 148 ++++++++--- .../RunnerPostReadRepositoryTest.java | 231 ++++++++++++++++++ .../RunnerPostRepositoryDeleteTest.java | 10 +- .../RunnerPostRepositoryReadTest.java | 10 +- .../read/RunnerPostRepositoryReadTest.java | 114 +++++++-- .../service/RunnerPostReadServiceTest.java | 154 ++++++++++++ .../service/RunnerPostServiceCreateTest.java | 17 +- .../service/RunnerPostServiceReadTest.java | 39 ++- .../service/RunnerPostServiceUpdateTest.java | 16 +- ...UpdateApplicantCancelationServiceTest.java | 5 +- .../runnerpost/vo/CuriousContentsTest.java | 16 ++ .../vo/ImplementedContentsTest.java | 16 ++ .../domain/runnerpost/vo/IsReviewedTest.java | 21 ++ .../runnerpost/vo/PostscriptContentsTest.java | 16 ++ .../supporter/SupporterFeedbackTest.java | 10 +- .../baton/domain/tag/RunnerPostTagTest.java | 15 +- .../baton/domain/tag/RunnerPostTagsTest.java | 2 +- .../java/touch/baton/domain/tag/TagTest.java | 16 +- .../RunnerPostTagRepositoryTest.java | 17 +- .../tag/repository/TagRepositoryReadTest.java | 116 +++++++++ .../tag/service/TagServiceReadTest.java | 51 ++++ .../domain/tag/vo/TagReducedNameTest.java | 45 ++++ .../SupporterTechnicalTagRepositoryTest.java | 15 +- .../fixture/domain/RefreshTokenFixture.java | 20 ++ .../fixture/domain/RunnerPostFixture.java | 92 ++++--- .../baton/fixture/domain/TagFixture.java | 2 + .../vo/AuthorizationHeaderFixture.java | 19 ++ .../fixture/vo/CuriousContentsFixture.java | 13 + .../baton/fixture/vo/DescriptionFixture.java | 2 +- .../baton/fixture/vo/ExpireDateFixture.java | 15 ++ .../vo/ImplementedContentsFixture.java | 13 + .../fixture/vo/PostscriptContentsFixture.java | 13 + .../touch/baton/fixture/vo/TokenFixture.java | 13 + .../auth/jwt/JwtEncoderAndDecoderTest.java | 22 +- .../touch/baton/util/TestDateFormatUtil.java | 14 ++ .../baton/src/test/resources/application.yml | 38 +++ 182 files changed, 6264 insertions(+), 1163 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 backend/baton/src/docs/asciidoc/GithubBranchCreateApi.adoc create mode 100644 backend/baton/src/docs/asciidoc/GithubOauthApi.adoc create mode 100644 backend/baton/src/docs/asciidoc/RefreshTokenApi.adoc create mode 100644 backend/baton/src/docs/asciidoc/RunnerReadApi.adoc create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/controller/MemberBranchController.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/service/dto/GithubBranchManageable.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/service/dto/GithubRepoNameRequest.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/AuthorizationHeader.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/repository/RefreshTokenRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/token/AccessToken.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/token/ExpireDate.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/token/RefreshToken.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/token/SocialToken.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/token/Token.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/token/Tokens.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/token/exception/RefreshTokenDomainException.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/RunnerPostReadController.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/repository/RunnerPostReadRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/repository/dto/ApplicantCountDto.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/repository/dto/ApplicantCountMappingDto.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/service/RunnerPostReadService.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/CuriousContents.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/ImplementedContents.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/IsReviewed.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/PostscriptContents.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/tag/controller/TagController.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/tag/controller/response/TagSearchResponse.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/tag/controller/response/TagSearchResponses.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/tag/service/TagService.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/tag/vo/TagReducedName.java create mode 100644 backend/baton/src/main/java/touch/baton/infra/exception/InfraException.java create mode 100644 backend/baton/src/main/java/touch/baton/infra/github/GithubBranchManager.java create mode 100644 backend/baton/src/main/java/touch/baton/infra/github/request/CreateBranchRequest.java create mode 100644 backend/baton/src/main/java/touch/baton/infra/github/response/ReadBranchInfoResponse.java create mode 100644 backend/baton/src/main/java/touch/baton/infra/github/response/ReadLastCommitInfoResponse.java create mode 100644 backend/baton/src/main/resources/db/migration/V0__init.sql create mode 100644 backend/baton/src/main/resources/db/migration/V20230910_1__add_new_contents_columns.sql create mode 100644 backend/baton/src/main/resources/db/migration/V20230910_2__set_contents_data_to_new_contents.sql create mode 100644 backend/baton/src/main/resources/db/migration/V20230910_3__drop_column_of_contents.sql create mode 100644 backend/baton/src/main/resources/db/migration/V20230913__alter_tag_name_column_utf8_bin.sql create mode 100644 backend/baton/src/main/resources/db/migration/V20230914__create_refresh_token_table.sql create mode 100644 backend/baton/src/main/resources/db/migration/V20230920_1__add_new_runner_post_is_reviewed_column.sql create mode 100644 backend/baton/src/main/resources/db/migration/V20230920_2__alter_runner_post_watched_count_column_name.sql create mode 100644 backend/baton/src/test/java/touch/baton/assure/common/JwtTestManager.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/common/OauthLoginTestManager.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/common/PathParams.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/common/QueryParams.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/member/MemberBranchAssuredSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/member/MemberBranchCreateAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/oauth/OauthAssuredSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/oauth/OauthCreateAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/oauth/OauthRefreshTokenAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/repository/TestMemberRepository.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/repository/TestRefreshTokenRepository.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/repository/TestRunnerPostReadRepository.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/repository/TestRunnerPostRepository.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/repository/TestRunnerRepository.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/repository/TestSupporterRepository.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/repository/TestSupporterRunnerPostRepository.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/repository/TestTagRepository.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/repository/TestTechnicalTagRepository.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/tag/TagAssuredSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/tag/TagReadAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/config/infra/auth/MockAuthTestConfig.java create mode 100644 backend/baton/src/test/java/touch/baton/config/infra/auth/jwt/MockJwtConfig.java create mode 100644 backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/MockRefreshTokenConfig.java create mode 100644 backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/authcode/MockAuthCodeRequestUrlProviderCompositeConfig.java create mode 100644 backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/authcode/MockAuthCodes.java create mode 100644 backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/client/MockOauthInformationClientCompositeConfig.java create mode 100644 backend/baton/src/test/java/touch/baton/config/infra/github/MockGithubBranchServiceConfig.java create mode 100644 backend/baton/src/test/java/touch/baton/document/github/GithubBranchApiTest.java create mode 100644 backend/baton/src/test/java/touch/baton/document/oauth/token/RefreshTokenApiTest.java create mode 100644 backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadSearchApiTest.java create mode 100644 backend/baton/src/test/java/touch/baton/document/runnerpost/read/TagReadApiTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/common/vo/TagNameTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/feedback/repository/SupporterFeedbackRepositoryTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/oauth/repository/RefreshTokenRepositoryTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/oauth/service/OauthServiceUpdateTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/oauth/token/RefreshTokenTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/oauth/vo/AuthorizationHeaderTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/RunnerPostReadRepositoryTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostReadServiceTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/CuriousContentsTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/ImplementedContentsTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/IsReviewedTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/PostscriptContentsTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/tag/repository/TagRepositoryReadTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/tag/service/TagServiceReadTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/tag/vo/TagReducedNameTest.java create mode 100644 backend/baton/src/test/java/touch/baton/fixture/domain/RefreshTokenFixture.java create mode 100644 backend/baton/src/test/java/touch/baton/fixture/vo/AuthorizationHeaderFixture.java create mode 100644 backend/baton/src/test/java/touch/baton/fixture/vo/CuriousContentsFixture.java create mode 100644 backend/baton/src/test/java/touch/baton/fixture/vo/ExpireDateFixture.java create mode 100644 backend/baton/src/test/java/touch/baton/fixture/vo/ImplementedContentsFixture.java create mode 100644 backend/baton/src/test/java/touch/baton/fixture/vo/PostscriptContentsFixture.java create mode 100644 backend/baton/src/test/java/touch/baton/fixture/vo/TokenFixture.java create mode 100644 backend/baton/src/test/java/touch/baton/util/TestDateFormatUtil.java create mode 100644 backend/baton/src/test/resources/application.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..455ae6b83 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,5 @@ +# Frontend +/frontend/ @tkdrb12 @gyeongza @guridaek + +# Backend +/backend/ @shb03323 @hyena0608 @cookienc @eunbii0213 diff --git a/.github/workflows/dev-be-ci-cd-push.yml b/.github/workflows/dev-be-ci-cd-push.yml index 4c522bb27..b4d331781 100644 --- a/.github/workflows/dev-be-ci-cd-push.yml +++ b/.github/workflows/dev-be-ci-cd-push.yml @@ -46,7 +46,7 @@ jobs: run: docker push 2023baton/2023baton deploy: - runs-on: [self-hosted, Linux, ARM64] + runs-on: [ self-hosted, Linux, ARM64, dev ] needs: build steps: diff --git a/backend/baton/src/docs/asciidoc/GithubBranchCreateApi.adoc b/backend/baton/src/docs/asciidoc/GithubBranchCreateApi.adoc new file mode 100644 index 000000000..99a4b7a03 --- /dev/null +++ b/backend/baton/src/docs/asciidoc/GithubBranchCreateApi.adoc @@ -0,0 +1,29 @@ +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/github-branch-api-test/create-member-branch/http-request.adoc[] + +===== *Http Request Headers* + +include::{snippets}/../../build/generated-snippets/github-branch-api-test/create-member-branch/request-headers.adoc[] + +===== *Http Response* + +include::{snippets}/../../build/generated-snippets/github-branch-api-test/create-member-branch/http-response.adoc[] + +===== *Http Response Headers* + +include::{snippets}/../../build/generated-snippets/github-branch-api-test/create-member-branch/response-headers.adoc[] diff --git a/backend/baton/src/docs/asciidoc/GithubOauthApi.adoc b/backend/baton/src/docs/asciidoc/GithubOauthApi.adoc new file mode 100644 index 000000000..ab6c85a3c --- /dev/null +++ b/backend/baton/src/docs/asciidoc/GithubOauthApi.adoc @@ -0,0 +1,37 @@ +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 + +==== *깃허브 소셜 로그인 redirect API* + +===== *Http Request* + +include::{snippets}/../../build/generated-snippets/github-oauth-api-test/github_login/http-request.adoc[] + +===== *Http Request Path Paramemters* + +include::{snippets}/../../build/generated-snippets/github-oauth-api-test/github_login/path-parameters.adoc[] + +===== *Http Request Query Paramemters* + +include::{snippets}/../../build/generated-snippets/github-oauth-api-test/github_login/query-parameters.adoc[] + +===== *Http Response* + +include::{snippets}/../../build/generated-snippets/github-oauth-api-test/github_login/http-response.adoc[] + +===== *Http Response Headers* + +include::{snippets}/../../build/generated-snippets/github-oauth-api-test/github_login/response-headers.adoc[] + +===== *Http Response Cookies* + +include::{snippets}/../../build/generated-snippets/github-oauth-api-test/github_login/response-cookies.adoc[] diff --git a/backend/baton/src/docs/asciidoc/RefreshTokenApi.adoc b/backend/baton/src/docs/asciidoc/RefreshTokenApi.adoc new file mode 100644 index 000000000..f40d928f2 --- /dev/null +++ b/backend/baton/src/docs/asciidoc/RefreshTokenApi.adoc @@ -0,0 +1,37 @@ +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/refresh-token-api-test/refresh/http-request.adoc[] + +===== *Http Request Headers* + +include::{snippets}/../../build/generated-snippets/refresh-token-api-test/refresh/request-headers.adoc[] + +===== *Http Request Cookies* + +include::{snippets}/../../build/generated-snippets/refresh-token-api-test/refresh/request-cookies.adoc[] + +===== *Http Response* + +include::{snippets}/../../build/generated-snippets/refresh-token-api-test/refresh/http-response.adoc[] + +===== *Http Response Headers* + +include::{snippets}/../../build/generated-snippets/refresh-token-api-test/refresh/response-headers.adoc[] + +===== *Http Response Cookies* + +include::{snippets}/../../build/generated-snippets/refresh-token-api-test/refresh/response-cookies.adoc[] diff --git a/backend/baton/src/docs/asciidoc/RunnerPostReadApi.adoc b/backend/baton/src/docs/asciidoc/RunnerPostReadApi.adoc index 21b2bf780..ba10d0571 100644 --- a/backend/baton/src/docs/asciidoc/RunnerPostReadApi.adoc +++ b/backend/baton/src/docs/asciidoc/RunnerPostReadApi.adoc @@ -36,19 +36,19 @@ include::{snippets}/../../build/generated-snippets/runner-post-read-one-api-test ===== *Http Request* -include::{snippets}/../../build/generated-snippets/runner-post-read-all-api-test/read-all-runner-posts/http-request.adoc[] +include::{snippets}/../../build/generated-snippets/runner-post-read-all-api-test/read-runner-posts-by-review-status/http-request.adoc[] ===== *Http Request Query Parameters* -include::{snippets}/../../build/generated-snippets/runner-post-read-all-api-test/read-all-runner-posts/query-parameters.adoc[] +include::{snippets}/../../build/generated-snippets/runner-post-read-all-api-test/read-runner-posts-by-review-status/query-parameters.adoc[] ===== *Http Response* -include::{snippets}/../../build/generated-snippets/runner-post-read-all-api-test/read-all-runner-posts/http-response.adoc[] +include::{snippets}/../../build/generated-snippets/runner-post-read-all-api-test/read-runner-posts-by-review-status/http-response.adoc[] ===== *Http Response Fields* -include::{snippets}/../../build/generated-snippets/runner-post-read-all-api-test/read-all-runner-posts/response-fields.adoc[] +include::{snippets}/../../build/generated-snippets/runner-post-read-all-api-test/read-runner-posts-by-review-status/response-fields.adoc[] ==== *러너 마이페이지 게시글 조회 API* @@ -129,3 +129,39 @@ include::{snippets}/../../build/generated-snippets/runner-post-read-of-supporter ===== *Http Response Fields* include::{snippets}/../../build/generated-snippets/runner-post-read-of-supporter-by-guest-api-test/read-referenced-by-supporter/response-fields.adoc[] + +==== *태그 이름과 리뷰 상태를 조건으로 러너 게시글 페이징 조회 API* + +===== *Http Request* + +include::{snippets}/../../build/generated-snippets/runner-post-read-search-api-test/read-runner-posts-by-tag-names-and-review-status/http-request.adoc[] + +===== *Http Request Query Parameters* + +include::{snippets}/../../build/generated-snippets/runner-post-read-search-api-test/read-runner-posts-by-tag-names-and-review-status/query-parameters.adoc[] + +===== *Http Response* + +include::{snippets}/../../build/generated-snippets/runner-post-read-search-api-test/read-runner-posts-by-tag-names-and-review-status/http-response.adoc[] + +===== *Http Response Fields* + +include::{snippets}/../../build/generated-snippets/runner-post-read-search-api-test/read-runner-posts-by-tag-names-and-review-status/response-fields.adoc[] + +==== *태그 검색 API* + +===== *Http Request* + +include::{snippets}/../../build/generated-snippets/tag-read-api-test/read-tags/http-request.adoc[] + +===== *Http Request Query Parameters* + +include::{snippets}/../../build/generated-snippets/tag-read-api-test/read-tags/query-parameters.adoc[] + +===== *Http Response* + +include::{snippets}/../../build/generated-snippets/tag-read-api-test/read-tags/http-response.adoc[] + +===== *Http Response Fields* + +include::{snippets}/../../build/generated-snippets/tag-read-api-test/read-tags/response-fields.adoc[] diff --git a/backend/baton/src/docs/asciidoc/RunnerReadApi.adoc b/backend/baton/src/docs/asciidoc/RunnerReadApi.adoc new file mode 100644 index 000000000..0a57d96bb --- /dev/null +++ b/backend/baton/src/docs/asciidoc/RunnerReadApi.adoc @@ -0,0 +1,26 @@ +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-read-by-runner-id-api-test/read-runner-profile/http-request.adoc[] + +===== *Http Response* + +include::{snippets}/../../build/generated-snippets/runner-read-by-runner-id-api-test/read-runner-profile/http-response.adoc[] + +===== *Http Response Fields* + +include::{snippets}/../../build/generated-snippets/runner-read-by-runner-id-api-test/read-runner-profile/response-fields.adoc[] + diff --git a/backend/baton/src/docs/asciidoc/index.adoc b/backend/baton/src/docs/asciidoc/index.adoc index d3ee20cfe..fc596cefa 100644 --- a/backend/baton/src/docs/asciidoc/index.adoc +++ b/backend/baton/src/docs/asciidoc/index.adoc @@ -10,6 +10,17 @@ endif::[] = Baton-API +== *[ 깃허브 레포 ]* + +=== *사용자 깃허브 브랜치 생성* + +include::GithubBranchCreateApi.adoc[] + +== *[ 로그인 ]* + +include::GithubOauthApi.adoc[] +include::RefreshTokenApi.adoc[] + == *[ 프로필 ]* === *사용자 프로필 조회* @@ -18,7 +29,7 @@ include::MemberLoginReadApi.adoc[] === *러너 프로필 조회* -include::RunneReadApi.adoc[] +include::RunnerReadApi.adoc[] === *러너 프로필 수정* diff --git a/backend/baton/src/main/java/touch/baton/common/LoggerAspect.java b/backend/baton/src/main/java/touch/baton/common/LoggerAspect.java index 63de093e4..58a5cf69e 100644 --- a/backend/baton/src/main/java/touch/baton/common/LoggerAspect.java +++ b/backend/baton/src/main/java/touch/baton/common/LoggerAspect.java @@ -2,17 +2,23 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; +import java.util.Arrays; import java.util.Objects; +import java.util.stream.Collectors; @Slf4j +@Profile("!test") @Aspect @Component public class LoggerAspect { @@ -25,36 +31,50 @@ public class LoggerAspect { public void logInfo() { } - @Around(value = "logInfo()") - public Object printLog(final ProceedingJoinPoint joinPoint) { + @Before("logInfo()") + public void requestLog(final JoinPoint joinPoint) { final HttpServletRequest request = getRequest(); final String signatureName = getSignatureName(joinPoint); - log.info(">>>>> API start [" + signatureName + "() from " - + request.getRemoteAddr() + "] by " - + request.getMethod() + " " - + request.getRequestURI()); - - final long startTime = System.currentTimeMillis(); - Object proceed = process(joinPoint, request, signatureName); - final long timeDiff = System.currentTimeMillis() - startTime; - log.info("시간차이(m) : {}", timeDiff); - return proceed; + log.info(">>>>> API start [{}() from {}] by {} {}", + signatureName, request.getRemoteAddr(), request.getMethod(), request.getRequestURI()); + } + + @AfterReturning(value = "logInfo()", returning = "returnObj") + public void after(final JoinPoint joinPoint, final Object returnObj) { + final HttpServletRequest request = getRequest(); + final String signatureName = getSignatureName(joinPoint); + log.info("\n>>>>> API finish [{}() from {}] by {} {} \n" + + ">>>>> API return value = {}", + signatureName, request, request.getMethod(), request.getRequestURI(), + returnObj); + } + + @AfterThrowing(value = "logInfo()", throwing = "exception") + public void afterThrowing(final JoinPoint joinPoint, final Exception exception) { + final HttpServletRequest request = getRequest(); + final String signatureName = getSignatureName(joinPoint); + log.warn(""" + \n + >>>>> API ERROR [{}() from {}] by {} {} + >>>>> ERROR MESSAGE = {} + >>>>> STACK TRACE = {} + """, + signatureName, request.getRemoteAddr(), request.getMethod(), request.getRequestURI(), + exception.getMessage(), + convertPrettyStackTrace(exception.getStackTrace())); } private HttpServletRequest getRequest() { return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); } - private String getSignatureName(final ProceedingJoinPoint joinPoint) { + private String getSignatureName(final JoinPoint joinPoint) { return joinPoint.getSignature().getDeclaringType().getSimpleName() + "." + joinPoint.getSignature().getName(); } - private Object process(final ProceedingJoinPoint joinPoint, final HttpServletRequest request, final String signatureName) { - try { - return joinPoint.proceed(); - } catch (Throwable e) { - log.error(">>>>> controller start [" + signatureName + "() from " + request.getRemoteAddr() + "] with Error[" + e.getMessage() + "]"); - throw new RuntimeException("에러 나요."); - } + private String convertPrettyStackTrace(final StackTraceElement[] stackTraceElements) { + return Arrays.stream(stackTraceElements) + .map(StackTraceElement::toString) + .collect(Collectors.joining("\n")); } } diff --git a/backend/baton/src/main/java/touch/baton/domain/common/GlobalControllerAdvice.java b/backend/baton/src/main/java/touch/baton/domain/common/GlobalControllerAdvice.java index 133e2a9bd..0a62662ac 100644 --- a/backend/baton/src/main/java/touch/baton/domain/common/GlobalControllerAdvice.java +++ b/backend/baton/src/main/java/touch/baton/domain/common/GlobalControllerAdvice.java @@ -1,6 +1,7 @@ package touch.baton.domain.common; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ValidationException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.TypeMismatchException; import org.springframework.http.HttpHeaders; @@ -31,6 +32,16 @@ @RestControllerAdvice public class GlobalControllerAdvice { + @ExceptionHandler(ValidationException.class) + public ResponseEntity handleValidation(final HttpServletRequest request, + final ClientRequestException e + ) { + LoggerUtils.logWarn(request, e); + return ResponseEntity + .status(e.getErrorCode().getHttpStatus()) + .body(ErrorResponse.from(e)); + } + @ExceptionHandler(ClientRequestException.class) public ResponseEntity handleClientRequest(final HttpServletRequest request, final ClientRequestException e @@ -72,4 +83,12 @@ public ResponseEntity handleBaseException(final HttpServlet LoggerUtils.logWarn(httpServletRequest, ex); return ResponseEntity.internalServerError().body(ServerErrorResponse.from(ex)); } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(final HttpServletRequest httpServletRequest, + final BaseException ex + ) { + LoggerUtils.logWarn(httpServletRequest, ex); + return ResponseEntity.internalServerError().body(ServerErrorResponse.unExpected()); + } } 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 c3e6e3ff6..1ab1a0a92 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 @@ -7,7 +7,7 @@ public enum ClientErrorCode { TITLE_IS_NULL(HttpStatus.BAD_REQUEST, "RP001", "제목을 입력해주세요."), PULL_REQUEST_URL_IS_NULL(HttpStatus.BAD_REQUEST, "RP002", "PR 주소를 입력해주세요."), DEADLINE_IS_NULL(HttpStatus.BAD_REQUEST, "RP003", "마감일을 입력해주세요."), - CONTENTS_ARE_NULL(HttpStatus.BAD_REQUEST, "RP004", "내용을 입력해주세요."), + IMPLEMENTED_CONTENTS_ARE_NULL(HttpStatus.BAD_REQUEST, "RP004", "구현 내용을 입력해주세요."), CONTENTS_OVERFLOW(HttpStatus.BAD_REQUEST, "RP005", "내용은 1000자 까지 입력해주세요."), PAST_DEADLINE(HttpStatus.BAD_REQUEST, "RP006", "마감일은 오늘보다 과거일 수 없습니다."), RUNNER_POST_NOT_FOUND(HttpStatus.NOT_FOUND, "RP007", "존재하지 않는 게시물입니다."), @@ -15,6 +15,8 @@ public enum ClientErrorCode { ASSIGN_SUPPORTER_ID_IS_NULL(HttpStatus.BAD_REQUEST, "RP009", "선택한 서포터의 식별자를 입력해주세요."), APPLICANT_MESSAGE_IS_OVERFLOW(HttpStatus.BAD_REQUEST, "RP010", "서포터 지원 메시지는 500자 까지 입력해주세요."), PULL_REQUEST_URL_IS_NOT_URL(HttpStatus.BAD_REQUEST, "RP011", "올바른 PR 주소를 입력해주세요."), + CURIOUS_CONTENTS_ARE_NULL(HttpStatus.BAD_REQUEST, "RP012", "궁금한 내용을 입력해주세요."), + POSTSCRIPT_CONTENTS_ARE_NULL(HttpStatus.BAD_REQUEST, "RP013", "참고 사항을 입력해주세요."), REVIEW_TYPE_IS_NULL(HttpStatus.BAD_REQUEST, "FB001", "만족도를 입력해주세요."), SUPPORTER_ID_IS_NULL(HttpStatus.BAD_REQUEST, "FB002", "서포터 식별자를 입력해주세요."), @@ -35,7 +37,16 @@ public enum ClientErrorCode { JWT_SIGNATURE_IS_WRONG(HttpStatus.UNAUTHORIZED, "JW001", "시그니처가 다른 잘못된 JWT 입니다."), JWT_FORM_IS_WRONG(HttpStatus.UNAUTHORIZED, "JW002", "잘못 생성된 JWT 로 디코딩 할 수 없습니다."), JWT_CLAIM_IS_WRONG(HttpStatus.UNAUTHORIZED, "JW003", "JWT 에 기대한 정보를 모두 포함하고 있지 않습니다."), - JWT_CLAIM_SOCIAL_ID_IS_WRONG(HttpStatus.UNAUTHORIZED, "JW004", "사용자의 잘못된 소셜 아이디(SocialId) 정보를 가진 JWT 입니다."); + JWT_CLAIM_SOCIAL_ID_IS_WRONG(HttpStatus.UNAUTHORIZED, "JW004", "사용자의 잘못된 소셜 아이디(SocialId) 정보를 가진 JWT 입니다."), + JWT_CLAIM_IS_ALREADY_EXPIRED(HttpStatus.UNAUTHORIZED, "JW005", "기간이 만료된 JWT 입니다."), + REFRESH_TOKEN_IS_NOT_FOUND(HttpStatus.UNAUTHORIZED, "JW007", "해당 사용자의 Refresh Token이 존재하지 않습니다."), + ACCESS_TOKEN_AND_REFRESH_TOKEN_HAVE_DIFFERENT_OWNER(HttpStatus.UNAUTHORIZED, "JW008", "Access Token 과 Refresh Token 의 주인이 다릅니다."), + REFRESH_TOKEN_IS_ALREADY_EXPIRED(HttpStatus.UNAUTHORIZED, "JW009", "기간이 만료된 Refresh Token 입니다."), + REFRESH_TOKEN_IS_NOT_NULL(HttpStatus.BAD_REQUEST, "JW010", "Refresh Token 은 비어 있을 수 없습니다."), + + DUPLICATED_BRANCH_NAME(HttpStatus.BAD_REQUEST, "BR001", "이미 존재하는 이름의 브랜치입니다."), + REPO_NAME_IS_NULL(HttpStatus.BAD_REQUEST, "BR002", "레포지토리 이름을 입력해주세요."), + REPO_NOT_FOUND(HttpStatus.NOT_FOUND, "BR003", "레포지토리를 찾을 수 없습니다."); private final HttpStatus httpStatus; private final String errorCode; diff --git a/backend/baton/src/main/java/touch/baton/domain/common/vo/WatchedCount.java b/backend/baton/src/main/java/touch/baton/domain/common/vo/WatchedCount.java index 39683032b..a1edb67a1 100644 --- a/backend/baton/src/main/java/touch/baton/domain/common/vo/WatchedCount.java +++ b/backend/baton/src/main/java/touch/baton/domain/common/vo/WatchedCount.java @@ -18,7 +18,7 @@ public class WatchedCount { private static final String DEFAULT_VALUE = "0"; @ColumnDefault(DEFAULT_VALUE) - @Column(name = "watch_count", nullable = false) + @Column(name = "watched_count", nullable = false) private int value; public WatchedCount(final int value) { diff --git a/backend/baton/src/main/java/touch/baton/domain/feedback/repository/SupporterFeedbackRepository.java b/backend/baton/src/main/java/touch/baton/domain/feedback/repository/SupporterFeedbackRepository.java index 3fcbfa163..66e62d5f7 100644 --- a/backend/baton/src/main/java/touch/baton/domain/feedback/repository/SupporterFeedbackRepository.java +++ b/backend/baton/src/main/java/touch/baton/domain/feedback/repository/SupporterFeedbackRepository.java @@ -4,4 +4,6 @@ import touch.baton.domain.feedback.SupporterFeedback; public interface SupporterFeedbackRepository extends JpaRepository { + + boolean existsByRunnerPostIdAndSupporterId(final Long runnerPostId, final Long supporterId); } diff --git a/backend/baton/src/main/java/touch/baton/domain/feedback/service/FeedbackService.java b/backend/baton/src/main/java/touch/baton/domain/feedback/service/FeedbackService.java index b7a18b0d1..37e872239 100644 --- a/backend/baton/src/main/java/touch/baton/domain/feedback/service/FeedbackService.java +++ b/backend/baton/src/main/java/touch/baton/domain/feedback/service/FeedbackService.java @@ -1,6 +1,5 @@ package touch.baton.domain.feedback.service; - import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -33,14 +32,17 @@ public Long createSupporterFeedback(final Runner runner, final SupporterFeedBack final RunnerPost foundRunnerPost = runnerPostRepository.findById(request.runnerPostId()) .orElseThrow(() -> new FeedbackBusinessException("러너 게시글을 찾을 수 없습니다.")); + if (supporterFeedbackRepository.existsByRunnerPostIdAndSupporterId(foundRunnerPost.getId(), foundSupporter.getId())) { + throw new FeedbackBusinessException("서포터에 대한 피드백을 작성했으면 추가적인 피드백을 남길 수 없습니다."); + } if (foundRunnerPost.isNotOwner(runner)) { throw new FeedbackBusinessException("리뷰 글을 작성한 주인만 글을 작성할 수 있습니다."); } - if (foundRunnerPost.isDifferentSupporter(foundSupporter)) { throw new FeedbackBusinessException("리뷰를 작성한 서포터에 대해서만 피드백을 작성할 수 있습니다."); } + foundRunnerPost.finishFeedback(); final SupporterFeedback supporterFeedback = SupporterFeedback.builder() .reviewType(ReviewType.valueOf(request.reviewType())) .description(new Description(String.join(DELIMITER, request.descriptions()))) diff --git a/backend/baton/src/main/java/touch/baton/domain/member/controller/MemberBranchController.java b/backend/baton/src/main/java/touch/baton/domain/member/controller/MemberBranchController.java new file mode 100644 index 000000000..b48dcea41 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/controller/MemberBranchController.java @@ -0,0 +1,32 @@ +package touch.baton.domain.member.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import touch.baton.domain.member.Member; +import touch.baton.domain.member.service.dto.GithubBranchManageable; +import touch.baton.domain.member.service.dto.GithubRepoNameRequest; +import touch.baton.domain.oauth.controller.resolver.AuthMemberPrincipal; + +import java.net.URI; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/branch") +@RestController +public class MemberBranchController { + + private final GithubBranchManageable githubBranchManageable; + + @PostMapping + public ResponseEntity createMemberBranch(@AuthMemberPrincipal final Member member, + @Valid @RequestBody final GithubRepoNameRequest githubRepoNameRequest + ) { + githubBranchManageable.createBranch(githubRepoNameRequest.repoName(), member.getSocialId().getValue()); + final URI redirectUri = URI.create("/api/v1/profile/me"); + return ResponseEntity.created(redirectUri).build(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/service/dto/GithubBranchManageable.java b/backend/baton/src/main/java/touch/baton/domain/member/service/dto/GithubBranchManageable.java new file mode 100644 index 000000000..0419a2b89 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/service/dto/GithubBranchManageable.java @@ -0,0 +1,6 @@ +package touch.baton.domain.member.service.dto; + +public interface GithubBranchManageable { + + void createBranch(final String repoName, final String newBranchName); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/service/dto/GithubRepoNameRequest.java b/backend/baton/src/main/java/touch/baton/domain/member/service/dto/GithubRepoNameRequest.java new file mode 100644 index 000000000..11cb1aab6 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/service/dto/GithubRepoNameRequest.java @@ -0,0 +1,7 @@ +package touch.baton.domain.member.service.dto; + +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.validator.ValidNotNull; + +public record GithubRepoNameRequest(@ValidNotNull(clientErrorCode = ClientErrorCode.REPO_NAME_IS_NULL) String repoName) { +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/AuthorizationHeader.java b/backend/baton/src/main/java/touch/baton/domain/oauth/AuthorizationHeader.java new file mode 100644 index 000000000..16fd4f7cd --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/AuthorizationHeader.java @@ -0,0 +1,27 @@ +package touch.baton.domain.oauth; + +public class AuthorizationHeader { + + private static final String BEARER = "Bearer "; + + private final String value; + + public AuthorizationHeader(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (value == null) { + throw new IllegalArgumentException("AuthorizationHeader 의 value 는 null 일 수 없습니다."); + } + } + + public String parseBearerAccessToken() { + return value.substring(BEARER.length()); + } + + public boolean isNotBearerAuth() { + return !value.startsWith(BEARER); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/OauthInformation.java b/backend/baton/src/main/java/touch/baton/domain/oauth/OauthInformation.java index d6762483b..1288ecfb1 100644 --- a/backend/baton/src/main/java/touch/baton/domain/oauth/OauthInformation.java +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/OauthInformation.java @@ -1,6 +1,5 @@ package touch.baton.domain.oauth; - import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -10,6 +9,7 @@ import touch.baton.domain.member.vo.MemberName; import touch.baton.domain.member.vo.OauthId; import touch.baton.domain.member.vo.SocialId; +import touch.baton.domain.oauth.token.SocialToken; import static lombok.AccessLevel.PROTECTED; diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/authcode/AuthCodeRequestUrlProviderComposite.java b/backend/baton/src/main/java/touch/baton/domain/oauth/authcode/AuthCodeRequestUrlProviderComposite.java index f25ff3ade..eba86ab74 100644 --- a/backend/baton/src/main/java/touch/baton/domain/oauth/authcode/AuthCodeRequestUrlProviderComposite.java +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/authcode/AuthCodeRequestUrlProviderComposite.java @@ -1,5 +1,6 @@ package touch.baton.domain.oauth.authcode; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import touch.baton.domain.common.exception.ClientErrorCode; import touch.baton.domain.oauth.OauthType; @@ -12,6 +13,7 @@ import static java.util.function.Function.identity; +@Profile("!test") @Component public class AuthCodeRequestUrlProviderComposite { diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/client/OauthInformationClientComposite.java b/backend/baton/src/main/java/touch/baton/domain/oauth/client/OauthInformationClientComposite.java index c93c09f07..0a657aa48 100644 --- a/backend/baton/src/main/java/touch/baton/domain/oauth/client/OauthInformationClientComposite.java +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/client/OauthInformationClientComposite.java @@ -1,5 +1,6 @@ package touch.baton.domain.oauth.client; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import touch.baton.domain.common.exception.ClientErrorCode; import touch.baton.domain.oauth.OauthInformation; @@ -13,6 +14,7 @@ import static java.util.function.Function.identity; +@Profile("!test") @Component public class OauthInformationClientComposite { diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/controller/OauthController.java b/backend/baton/src/main/java/touch/baton/domain/oauth/controller/OauthController.java index cc5e35801..98d4383df 100644 --- a/backend/baton/src/main/java/touch/baton/domain/oauth/controller/OauthController.java +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/controller/OauthController.java @@ -1,17 +1,30 @@ package touch.baton.domain.oauth.controller; +import jakarta.annotation.Nullable; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.springframework.boot.web.server.Cookie.SameSite; +import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.ClientRequestException; +import touch.baton.domain.oauth.AuthorizationHeader; import touch.baton.domain.oauth.OauthType; import touch.baton.domain.oauth.service.OauthService; +import touch.baton.domain.oauth.token.RefreshToken; +import touch.baton.domain.oauth.token.Tokens; import java.io.IOException; +import java.time.Duration; +import java.time.LocalDateTime; import static org.springframework.http.HttpHeaders.AUTHORIZATION; import static org.springframework.http.HttpStatus.FOUND; @@ -35,11 +48,49 @@ public ResponseEntity redirectAuthCode(@PathVariable("oauthType") final Oa @GetMapping("/login/{oauthType}") public ResponseEntity login(@PathVariable final OauthType oauthType, - @RequestParam final String code + @RequestParam final String code, + final HttpServletResponse response ) { - final String jwtToken = oauthService.login(oauthType, code); + final Tokens tokens = oauthService.login(oauthType, code); + + setCookie(response, tokens.refreshToken()); + return ResponseEntity.ok() - .header(AUTHORIZATION, jwtToken) + .header(AUTHORIZATION, tokens.accessToken().getValue()) + .build(); + } + + @PostMapping("/refresh") + public ResponseEntity refreshJwt(@Nullable @CookieValue(required = false) final String refreshToken, + final HttpServletRequest request, + final HttpServletResponse response + ) { + if (request.getHeader(AUTHORIZATION) == null) { + throw new ClientRequestException(ClientErrorCode.OAUTH_AUTHORIZATION_VALUE_IS_NULL); + } + if (refreshToken == null || refreshToken.isBlank()) { + throw new ClientRequestException(ClientErrorCode.REFRESH_TOKEN_IS_NOT_NULL); + } + + final AuthorizationHeader authorizationHeader = new AuthorizationHeader(request.getHeader(AUTHORIZATION)); + + final Tokens tokens = oauthService.reissueAccessToken(authorizationHeader, refreshToken); + + setCookie(response, tokens.refreshToken()); + + return ResponseEntity.noContent() + .header(AUTHORIZATION, tokens.accessToken().getValue()) + .build(); + } + + private void setCookie(final HttpServletResponse response, final RefreshToken refreshToken) { + final ResponseCookie responseCookie = ResponseCookie.from("refreshToken", refreshToken.getToken().getValue()) + .httpOnly(true) + .secure(true) + .maxAge(Duration.between(LocalDateTime.now(), refreshToken.getExpireDate().getValue()).toSeconds()) + .sameSite(SameSite.NONE.attributeValue()) + .path("/") .build(); + response.addHeader("Set-Cookie", responseCookie.toString()); } } diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/controller/resolver/UserPrincipalArgumentResolver.java b/backend/baton/src/main/java/touch/baton/domain/oauth/controller/resolver/UserPrincipalArgumentResolver.java index 0570d2441..c855f9254 100644 --- a/backend/baton/src/main/java/touch/baton/domain/oauth/controller/resolver/UserPrincipalArgumentResolver.java +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/controller/resolver/UserPrincipalArgumentResolver.java @@ -8,6 +8,7 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.oauth.AuthorizationHeader; import touch.baton.domain.oauth.exception.OauthRequestException; import touch.baton.infra.auth.jwt.JwtDecoder; @@ -37,13 +38,12 @@ public Object resolveArgument(final MethodParameter parameter, return getGuest(); } - final String authHeader = webRequest.getHeader(AUTHORIZATION); - if (!authHeader.startsWith(BEARER)) { + final AuthorizationHeader authorization = new AuthorizationHeader(webRequest.getHeader(AUTHORIZATION)); + if (authorization.isNotBearerAuth()) { throw new OauthRequestException(ClientErrorCode.OAUTH_AUTHORIZATION_BEARER_TYPE_NOT_FOUND); } - final String token = authHeader.substring(BEARER.length()); - final Claims claims = jwtDecoder.parseJwtToken(token); + final Claims claims = jwtDecoder.parseAuthorizationHeader(authorization); final String socialId = claims.get("socialId", String.class); return getUser(socialId); diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/repository/RefreshTokenRepository.java b/backend/baton/src/main/java/touch/baton/domain/oauth/repository/RefreshTokenRepository.java new file mode 100644 index 000000000..b202ee63f --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/repository/RefreshTokenRepository.java @@ -0,0 +1,15 @@ +package touch.baton.domain.oauth.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import touch.baton.domain.member.Member; +import touch.baton.domain.oauth.token.RefreshToken; +import touch.baton.domain.oauth.token.Token; + +import java.util.Optional; + +public interface RefreshTokenRepository extends JpaRepository { + + Optional findByToken(final Token token); + + Optional findByMember(final Member member); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/service/OauthService.java b/backend/baton/src/main/java/touch/baton/domain/oauth/service/OauthService.java index 5c31f3219..a3c824c10 100644 --- a/backend/baton/src/main/java/touch/baton/domain/oauth/service/OauthService.java +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/service/OauthService.java @@ -1,28 +1,44 @@ package touch.baton.domain.oauth.service; +import io.jsonwebtoken.Claims; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import touch.baton.domain.common.vo.Introduction; +import org.springframework.transaction.annotation.Transactional; +import touch.baton.domain.common.exception.ClientErrorCode; import touch.baton.domain.member.Member; import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.SocialId; +import touch.baton.domain.oauth.AuthorizationHeader; import touch.baton.domain.oauth.OauthInformation; import touch.baton.domain.oauth.OauthType; import touch.baton.domain.oauth.authcode.AuthCodeRequestUrlProviderComposite; import touch.baton.domain.oauth.client.OauthInformationClientComposite; +import touch.baton.domain.oauth.exception.OauthRequestException; import touch.baton.domain.oauth.repository.OauthMemberRepository; import touch.baton.domain.oauth.repository.OauthRunnerRepository; import touch.baton.domain.oauth.repository.OauthSupporterRepository; +import touch.baton.domain.oauth.repository.RefreshTokenRepository; +import touch.baton.domain.oauth.token.AccessToken; +import touch.baton.domain.oauth.token.ExpireDate; +import touch.baton.domain.oauth.token.RefreshToken; +import touch.baton.domain.oauth.token.Token; +import touch.baton.domain.oauth.token.Tokens; import touch.baton.domain.runner.Runner; import touch.baton.domain.supporter.Supporter; import touch.baton.domain.supporter.vo.ReviewCount; import touch.baton.domain.technicaltag.SupporterTechnicalTags; +import touch.baton.infra.auth.jwt.JwtDecoder; import touch.baton.infra.auth.jwt.JwtEncoder; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Map; import java.util.Optional; +import java.util.UUID; @RequiredArgsConstructor +@Transactional(readOnly = true) @Service public class OauthService { @@ -31,13 +47,19 @@ public class OauthService { private final OauthMemberRepository oauthMemberRepository; private final OauthRunnerRepository oauthRunnerRepository; private final OauthSupporterRepository oauthSupporterRepository; + private final RefreshTokenRepository refreshTokenRepository; private final JwtEncoder jwtEncoder; + private final JwtDecoder jwtDecoder; + + @Value("${refresh_token.expire_minutes}") + private int refreshTokenExpireMinutes; public String readAuthCodeRedirect(final OauthType oauthType) { return authCodeRequestUrlProviderComposite.findRequestUrl(oauthType); } - public String login(final OauthType oauthType, final String code) { + @Transactional + public Tokens login(final OauthType oauthType, final String code) { final OauthInformation oauthInformation = oauthInformationClientComposite.fetchInformation(oauthType, code); final Optional maybeMember = oauthMemberRepository.findMemberByOauthId(oauthInformation.getOauthId()); @@ -45,11 +67,10 @@ public String login(final OauthType oauthType, final String code) { final Member savedMember = signUpMember(oauthInformation); saveNewRunner(savedMember); saveNewSupporter(savedMember); + return createTokens(oauthInformation.getSocialId(), savedMember); } - return jwtEncoder.jwtToken(Map.of( - "socialId", oauthInformation.getSocialId().getValue()) - ); + return createTokens(oauthInformation.getSocialId(), maybeMember.get()); } private Member signUpMember(final OauthInformation oauthInformation) { @@ -82,4 +103,62 @@ private Supporter saveNewSupporter(final Member member) { return oauthSupporterRepository.save(newSupporter); } + + private Tokens createTokens(final SocialId socialId, final Member member) { + final AccessToken accessToken = createAccessToken(socialId); + + final String randomTokens = UUID.randomUUID().toString(); + final Token token = new Token(randomTokens); + final LocalDateTime expireDate = LocalDateTime.now().plusMinutes(refreshTokenExpireMinutes); + final RefreshToken refreshToken = RefreshToken.builder() + .member(member) + .token(token) + .expireDate(new ExpireDate(expireDate)) + .build(); + + final Optional maybeRefreshToken = refreshTokenRepository.findByMember(member); + if (maybeRefreshToken.isPresent()) { + final RefreshToken findRefreshToken = maybeRefreshToken.get(); + findRefreshToken.updateToken(new Token(randomTokens), refreshTokenExpireMinutes); + return new Tokens(accessToken, findRefreshToken); + } + + refreshTokenRepository.save(refreshToken); + return new Tokens(accessToken, refreshToken); + } + + @Transactional + public Tokens reissueAccessToken(final AuthorizationHeader authHeader, final String refreshToken) { + final Claims claims = jwtDecoder.parseExpiredAuthorizationHeader(authHeader); + final SocialId socialId = new SocialId(claims.get("socialId", String.class)); + final Member findMember = oauthMemberRepository.findBySocialId(socialId) + .orElseThrow(() -> new OauthRequestException(ClientErrorCode.JWT_CLAIM_SOCIAL_ID_IS_WRONG)); + + final RefreshToken findRefreshToken = refreshTokenRepository.findByToken(new Token(refreshToken)) + .orElseThrow(() -> new OauthRequestException(ClientErrorCode.REFRESH_TOKEN_IS_NOT_FOUND)); + + if (findRefreshToken.isNotOwner(findMember)) { + throw new OauthRequestException(ClientErrorCode.ACCESS_TOKEN_AND_REFRESH_TOKEN_HAVE_DIFFERENT_OWNER); + } + if (findRefreshToken.isExpired()) { + throw new OauthRequestException(ClientErrorCode.REFRESH_TOKEN_IS_ALREADY_EXPIRED); + } + + return reissueTokens(socialId, findRefreshToken); + } + + private Tokens reissueTokens(final SocialId socialId, final RefreshToken refreshToken) { + final AccessToken accessToken = createAccessToken(socialId); + + refreshToken.updateToken(new Token(UUID.randomUUID().toString()), refreshTokenExpireMinutes); + + return new Tokens(accessToken, refreshToken); + } + + private AccessToken createAccessToken(final SocialId socialId) { + final String jwtToken = jwtEncoder.jwtToken(Map.of( + "socialId", socialId.getValue()) + ); + return new AccessToken(jwtToken); + } } diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/token/AccessToken.java b/backend/baton/src/main/java/touch/baton/domain/oauth/token/AccessToken.java new file mode 100644 index 000000000..b0b5dc839 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/token/AccessToken.java @@ -0,0 +1,15 @@ +package touch.baton.domain.oauth.token; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode +@Getter +public class AccessToken { + + private final String value; + + public AccessToken(final String value) { + this.value = value; + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/token/ExpireDate.java b/backend/baton/src/main/java/touch/baton/domain/oauth/token/ExpireDate.java new file mode 100644 index 000000000..b20bf6af0 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/token/ExpireDate.java @@ -0,0 +1,40 @@ +package touch.baton.domain.oauth.token; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class ExpireDate { + + @Column(name = "expire_date", nullable = false) + private LocalDateTime value; + + public ExpireDate(final LocalDateTime value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final LocalDateTime value) { + if (value == null) { + throw new IllegalArgumentException("ExpireDate 의 value 는 null일 수 없습니다."); + } + } + + public void refreshExpireTokenDate(final int minutes) { + this.value = LocalDateTime.now().plusMinutes(minutes); + } + + public boolean isExpired() { + return LocalDateTime.now().isAfter(value); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/token/RefreshToken.java b/backend/baton/src/main/java/touch/baton/domain/oauth/token/RefreshToken.java new file mode 100644 index 000000000..0f42590f3 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/token/RefreshToken.java @@ -0,0 +1,77 @@ +package touch.baton.domain.oauth.token; + +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import touch.baton.domain.common.BaseEntity; +import touch.baton.domain.member.Member; +import touch.baton.domain.oauth.token.exception.RefreshTokenDomainException; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class RefreshToken extends BaseEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + @OneToOne(fetch = LAZY) + @JoinColumn(name = "member_id", foreignKey = @ForeignKey(name = "fk_refresh_token_to_member"), nullable = false) + private Member member; + + @Embedded + private Token token; + + @Embedded + private ExpireDate expireDate; + + @Builder + private RefreshToken(final Member member, final Token token, final ExpireDate expireDate) { + this(null, member, token, expireDate); + } + + private RefreshToken(final Long id, final Member member, final Token token, final ExpireDate expireDate) { + validateNotNull(member, token, expireDate); + this.id = id; + this.member = member; + this.token = token; + this.expireDate = expireDate; + } + + private void validateNotNull(final Member member, final Token token, final ExpireDate expireDate) { + if (member == null) { + throw new RefreshTokenDomainException("RefreshToken 의 member 는 null 일 수 없습니다."); + } + if (token == null) { + throw new RefreshTokenDomainException("RefreshToken 의 token 은 null 일 수 없습니다."); + } + if (expireDate == null) { + throw new RefreshTokenDomainException("RefreshToken 의 expireDate 는 null 일 수 없습니다."); + } + } + + public void updateToken(final Token token, final int expiredMinutes) { + this.token = token; + expireDate.refreshExpireTokenDate(expiredMinutes); + } + + public boolean isNotOwner(final Member member) { + return !this.member.equals(member); + } + + public boolean isExpired() { + return expireDate.isExpired(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/token/SocialToken.java b/backend/baton/src/main/java/touch/baton/domain/oauth/token/SocialToken.java new file mode 100644 index 000000000..f9e60cf03 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/token/SocialToken.java @@ -0,0 +1,15 @@ +package touch.baton.domain.oauth.token; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode +@Getter +public final class SocialToken { + + private final String value; + + public SocialToken(final String value) { + this.value = value; + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/token/Token.java b/backend/baton/src/main/java/touch/baton/domain/oauth/token/Token.java new file mode 100644 index 000000000..120ccaf79 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/token/Token.java @@ -0,0 +1,31 @@ +package touch.baton.domain.oauth.token; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.util.ObjectUtils; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class Token { + + @Column(name = "token", nullable = false, columnDefinition = "text") + private String value; + + public Token(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (ObjectUtils.isEmpty(value)) { + throw new IllegalArgumentException("RefreshToken 의 value 는 null이거나 비어있을 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/token/Tokens.java b/backend/baton/src/main/java/touch/baton/domain/oauth/token/Tokens.java new file mode 100644 index 000000000..aa7287134 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/token/Tokens.java @@ -0,0 +1,7 @@ +package touch.baton.domain.oauth.token; + +public record Tokens( + AccessToken accessToken, + RefreshToken refreshToken +) { +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/token/exception/RefreshTokenDomainException.java b/backend/baton/src/main/java/touch/baton/domain/oauth/token/exception/RefreshTokenDomainException.java new file mode 100644 index 000000000..6f72e72de --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/token/exception/RefreshTokenDomainException.java @@ -0,0 +1,10 @@ +package touch.baton.domain.oauth.token.exception; + +import touch.baton.domain.common.exception.DomainException; + +public class RefreshTokenDomainException extends DomainException { + + public RefreshTokenDomainException(final String message) { + super(message); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/RunnerPost.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/RunnerPost.java index c3e4f18e8..f20467a24 100644 --- a/backend/baton/src/main/java/touch/baton/domain/runnerpost/RunnerPost.java +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/RunnerPost.java @@ -13,13 +13,16 @@ import lombok.Getter; import lombok.NoArgsConstructor; import touch.baton.domain.common.BaseEntity; -import touch.baton.domain.common.vo.Contents; import touch.baton.domain.common.vo.Title; import touch.baton.domain.common.vo.WatchedCount; import touch.baton.domain.member.Member; import touch.baton.domain.runner.Runner; import touch.baton.domain.runnerpost.exception.RunnerPostDomainException; +import touch.baton.domain.runnerpost.vo.CuriousContents; import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.ImplementedContents; +import touch.baton.domain.runnerpost.vo.IsReviewed; +import touch.baton.domain.runnerpost.vo.PostscriptContents; import touch.baton.domain.runnerpost.vo.PullRequestUrl; import touch.baton.domain.runnerpost.vo.ReviewStatus; import touch.baton.domain.supporter.Supporter; @@ -53,7 +56,13 @@ public class RunnerPost extends BaseEntity { private Title title; @Embedded - private Contents contents; + private ImplementedContents implementedContents; + + @Embedded + private CuriousContents curiousContents; + + @Embedded + private PostscriptContents postscriptContents; @Embedded private PullRequestUrl pullRequestUrl; @@ -68,6 +77,9 @@ public class RunnerPost extends BaseEntity { @Column(nullable = false) private ReviewStatus reviewStatus = ReviewStatus.NOT_STARTED; + @Embedded + private IsReviewed isReviewed; + @ManyToOne(fetch = LAZY) @JoinColumn(name = "runner_id", foreignKey = @ForeignKey(name = "fk_runner_post_to_runner"), nullable = false) private Runner runner; @@ -81,97 +93,116 @@ public class RunnerPost extends BaseEntity { @Builder private RunnerPost(final Title title, - final Contents contents, + final ImplementedContents implementedContents, + final CuriousContents curiousContents, + final PostscriptContents postscriptContents, final PullRequestUrl pullRequestUrl, final Deadline deadline, final WatchedCount watchedCount, final ReviewStatus reviewStatus, + final IsReviewed isReviewed, final Runner runner, final Supporter supporter, final RunnerPostTags runnerPostTags ) { - this(null, title, contents, pullRequestUrl, deadline, watchedCount, reviewStatus, runner, supporter, runnerPostTags); + this(null, title, implementedContents, curiousContents, postscriptContents, pullRequestUrl, deadline, watchedCount, reviewStatus, isReviewed, runner, supporter, runnerPostTags); } private RunnerPost(final Long id, final Title title, - final Contents contents, + final ImplementedContents implementedContents, + final CuriousContents curiousContents, + final PostscriptContents postscriptContents, final PullRequestUrl pullRequestUrl, final Deadline deadline, final WatchedCount watchedCount, final ReviewStatus reviewStatus, + final IsReviewed isReviewed, final Runner runner, final Supporter supporter, final RunnerPostTags runnerPostTags ) { - validateNotNull(title, contents, pullRequestUrl, deadline, watchedCount, reviewStatus, runner, runnerPostTags); + validateNotNull(title, implementedContents, curiousContents, postscriptContents, pullRequestUrl, deadline, watchedCount, reviewStatus, isReviewed, runner, runnerPostTags); this.id = id; this.title = title; - this.contents = contents; + this.implementedContents = implementedContents; + this.curiousContents = curiousContents; + this.postscriptContents = postscriptContents; this.pullRequestUrl = pullRequestUrl; this.deadline = deadline; this.watchedCount = watchedCount; this.reviewStatus = reviewStatus; + this.isReviewed = isReviewed; this.runner = runner; this.supporter = supporter; this.runnerPostTags = runnerPostTags; } public static RunnerPost newInstance(final String title, - final String contents, + final String implementedContents, + final String curiousContents, + final String postscriptContents, final String pullRequestUrl, final LocalDateTime deadline, final Runner runner ) { return RunnerPost.builder() .title(new Title(title)) - .contents(new Contents(contents)) + .implementedContents(new ImplementedContents(implementedContents)) + .curiousContents(new CuriousContents(curiousContents)) + .postscriptContents(new PostscriptContents(postscriptContents)) .pullRequestUrl(new PullRequestUrl(pullRequestUrl)) .deadline(new Deadline(deadline)) - .runner(runner) - .runnerPostTags(new RunnerPostTags(new ArrayList<>())) .watchedCount(WatchedCount.zero()) .reviewStatus(NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) + .runner(runner) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) .build(); } private void validateNotNull(final Title title, - final Contents contents, + final ImplementedContents implementedContents, + final CuriousContents curiousContents, + final PostscriptContents postscriptContents, final PullRequestUrl pullRequestUrl, final Deadline deadline, final WatchedCount watchedCount, final ReviewStatus reviewStatus, + final IsReviewed isReviewed, final Runner runner, final RunnerPostTags runnerPostTags ) { if (Objects.isNull(title)) { throw new RunnerPostDomainException("RunnerPost 의 title 은 null 일 수 없습니다."); } - - if (Objects.isNull(contents)) { - throw new RunnerPostDomainException("RunnerPost 의 contents 는 null 일 수 없습니다."); + if (Objects.isNull(implementedContents)) { + throw new RunnerPostDomainException("RunnerPost 의 implementedContents 는 null 일 수 없습니다."); + } + if (Objects.isNull(curiousContents)) { + throw new RunnerPostDomainException("RunnerPost 의 curiousContents 는 null 일 수 없습니다."); + } + if (Objects.isNull(postscriptContents)) { + throw new RunnerPostDomainException("RunnerPost 의 postscriptContents 는 null 일 수 없습니다."); } - if (Objects.isNull(pullRequestUrl)) { throw new RunnerPostDomainException("RunnerPost 의 pullRequestUrl 은 null 일 수 없습니다."); } - if (Objects.isNull(deadline)) { throw new RunnerPostDomainException("RunnerPost 의 deadline 은 null 일 수 없습니다."); } - if (Objects.isNull(watchedCount)) { throw new RunnerPostDomainException("RunnerPost 의 watchedCount 는 null 일 수 없습니다."); } - if (Objects.isNull(reviewStatus)) { throw new RunnerPostDomainException("RunnerPost 의 reviewStatus 는 null 일 수 없습니다."); } - + if (Objects.isNull(isReviewed)) { + throw new RunnerPostDomainException("RunnerPost 의 isReviewed 는 null 일 수 없습니다."); + } if (Objects.isNull(runner)) { throw new RunnerPostDomainException("RunnerPost 의 runner 는 null 일 수 없습니다."); } - if (Objects.isNull(runnerPostTags)) { throw new RunnerPostDomainException("RunnerPost 의 runnerPostTags 는 null 일 수 없습니다."); } @@ -181,30 +212,14 @@ public void addAllRunnerPostTags(final List postTags) { runnerPostTags.addAll(postTags); } - public void appendRunnerPostTag(RunnerPostTag postTag) { - runnerPostTags.add(postTag); - } - - public void updateTitle(final Title title) { - this.title = title; - } - - public void updateContents(final Contents contents) { - this.contents = contents; - } - - public void updatePullRequestUrl(final PullRequestUrl pullRequestUrl) { - this.pullRequestUrl = pullRequestUrl; - } - - public void updateDeadLine(final Deadline deadline) { - this.deadline = deadline; - } - public void finishReview() { updateReviewStatus(DONE); } + public void finishFeedback() { + this.isReviewed = IsReviewed.reviewed(); + } + public void updateReviewStatus(final ReviewStatus other) { if (this.reviewStatus.isSame(NOT_STARTED) && other.isSame(IN_PROGRESS)) { throw new RunnerPostDomainException("ReviewStatus 를 수정하던 도중 NOT_STARTED 에서 IN_PROGRESS 로 리뷰 상태 정책을 원인으로 실패하였습니다."); @@ -274,10 +289,10 @@ public boolean isReviewStatusNotStarted() { } @Override - public boolean equals(Object o) { + public boolean equals(final Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - RunnerPost that = (RunnerPost) o; + final RunnerPost that = (RunnerPost) o; return Objects.equals(id, that.id); } 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 9f2f6aecb..803efd134 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 @@ -88,10 +88,11 @@ public ResponseEntity deleteByRunnerPostId(@AuthRunnerPrincipal final Runn } @GetMapping - public ResponseEntity> readAllRunnerPosts( - @PageableDefault(sort = "createdAt", direction = DESC) final Pageable pageable + public ResponseEntity> readRunnerPostsByReviewStatus( + @PageableDefault(sort = "id", direction = DESC) final Pageable pageable, + @RequestParam("reviewStatus") final ReviewStatus reviewStatus ) { - final Page pageRunnerPosts = runnerPostService.readAllRunnerPosts(pageable); + final Page pageRunnerPosts = runnerPostService.readRunnerPostsByReviewStatus(pageable, reviewStatus); final List foundRunnerPosts = pageRunnerPosts.getContent(); final List applicantCounts = collectApplicantCounts(pageRunnerPosts); final List responses = IntStream.range(0, foundRunnerPosts.size()) @@ -103,14 +104,14 @@ public ResponseEntity> readAllRunnerPost }).toList(); final Page pageResponse - = new PageImpl<>(responses, pageable, pageRunnerPosts.getTotalPages()); + = new PageImpl<>(responses, pageable, pageRunnerPosts.getTotalElements()); return ResponseEntity.ok(PageResponse.from(pageResponse)); } @GetMapping("/search") public ResponseEntity> readReferencedBySupporter( - @PageableDefault(sort = "createdAt", direction = DESC) final Pageable pageable, + @PageableDefault(sort = "id", direction = DESC) final Pageable pageable, @RequestParam("supporterId") final Long supporterId, @RequestParam("reviewStatus") final ReviewStatus reviewStatus ) { @@ -126,7 +127,7 @@ public ResponseEntity> re }).toList(); final Page pageResponse - = new PageImpl<>(responses, pageable, pageRunnerPosts.getTotalPages()); + = new PageImpl<>(responses, pageable, pageRunnerPosts.getTotalElements()); return ResponseEntity.ok(PageResponse.from(pageResponse)); } @@ -145,7 +146,7 @@ public ResponseEntity readSupporterRunnerPo @GetMapping("/me/supporter") public ResponseEntity> readRunnerPostsByLoginedSupporterAndReviewStatus( - @PageableDefault(sort = "createdAt", direction = DESC) final Pageable pageable, + @PageableDefault(sort = "id", direction = DESC) final Pageable pageable, @AuthSupporterPrincipal final Supporter supporter, @RequestParam("reviewStatus") final ReviewStatus reviewStatus ) { @@ -161,14 +162,14 @@ public ResponseEntity> re }).toList(); final Page pageResponse - = new PageImpl<>(responses, pageable, pageRunnerPosts.getTotalPages()); + = new PageImpl<>(responses, pageable, pageRunnerPosts.getTotalElements()); return ResponseEntity.ok(PageResponse.from(pageResponse)); } @GetMapping("/me/runner") public ResponseEntity> readRunnerMyPage( - @PageableDefault(sort = "createdAt", direction = DESC) final Pageable pageable, + @PageableDefault(sort = "id", direction = DESC) final Pageable pageable, @AuthRunnerPrincipal final Runner runner, @RequestParam("reviewStatus") final ReviewStatus reviewStatus ) { @@ -184,7 +185,7 @@ public ResponseEntity> readRunne ).toList(); final Page pageResponse - = new PageImpl<>(responses, pageable, pageRunnerPosts.getTotalPages()); + = new PageImpl<>(responses, pageable, pageRunnerPosts.getTotalElements()); return ResponseEntity.ok(PageResponse.from(pageResponse)); } diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/RunnerPostReadController.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/RunnerPostReadController.java new file mode 100644 index 000000000..8d1558e8d --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/RunnerPostReadController.java @@ -0,0 +1,71 @@ +package touch.baton.domain.runnerpost.controller; + +import jakarta.annotation.Nullable; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import touch.baton.domain.common.response.PageResponse; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.controller.response.RunnerPostResponse; +import touch.baton.domain.runnerpost.repository.dto.ApplicantCountMappingDto; +import touch.baton.domain.runnerpost.service.RunnerPostReadService; +import touch.baton.domain.runnerpost.service.RunnerPostService; +import touch.baton.domain.runnerpost.vo.ReviewStatus; + +import java.util.List; + +import static org.springframework.data.domain.Sort.Direction.DESC; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/posts/runner") +@RestController +public class RunnerPostReadController { + + private final RunnerPostReadService runnerPostReadService; + private final RunnerPostService runnerPostService; + + @GetMapping("/tags/search") + public ResponseEntity> readRunnerPostsByTagNamesAndReviewStatus( + @PageableDefault(sort = "id", direction = DESC) final Pageable pageable, + @Nullable @RequestParam(required = false) final String tagName, + @RequestParam final ReviewStatus reviewStatus + ) { + final Page pageRunnerPosts = getPageRunnerPosts(pageable, tagName, reviewStatus); + final ApplicantCountMappingDto applicantCountMapping = getApplicantCountMapping(pageRunnerPosts); + + final List responses = pageRunnerPosts.getContent().stream() + .map(runnerPost -> { + final Long foundApplicantCount = applicantCountMapping.getApplicantCountByRunnerPostId(runnerPost.getId()); + + return RunnerPostResponse.Simple.from(runnerPost, foundApplicantCount); + }).toList(); + + final PageImpl pageResponse + = new PageImpl<>(responses, pageable, pageRunnerPosts.getTotalElements()); + + return ResponseEntity.ok(PageResponse.from(pageResponse)); + } + + private Page getPageRunnerPosts(final Pageable pageable, final String tagName, final ReviewStatus reviewStatus) { + if (tagName == null || tagName.isBlank()) { + return runnerPostService.readRunnerPostsByReviewStatus(pageable, reviewStatus); + } + + return runnerPostReadService.readRunnerPostByTagNameAndReviewStatus(pageable, tagName, reviewStatus); + } + + private ApplicantCountMappingDto getApplicantCountMapping(final Page pageRunnerPosts) { + final List foundRunnerPostIds = pageRunnerPosts.stream() + .map(RunnerPost::getId) + .toList(); + + return runnerPostReadService.readApplicantCountMappingByRunnerPostIds(foundRunnerPostIds); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/response/RunnerPostResponse.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/response/RunnerPostResponse.java index ea159a866..2d1ea2467 100644 --- a/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/response/RunnerPostResponse.java +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/response/RunnerPostResponse.java @@ -12,7 +12,9 @@ public record RunnerPostResponse() { public record Detail(Long runnerPostId, String title, - String contents, + String implementedContents, + String curiousContents, + String postscriptContents, String pullRequestUrl, LocalDateTime deadline, int watchedCount, @@ -32,7 +34,9 @@ public static Detail of(final RunnerPost runnerPost, return new Detail( runnerPost.getId(), runnerPost.getTitle().getValue(), - runnerPost.getContents().getValue(), + runnerPost.getImplementedContents().getValue(), + runnerPost.getCuriousContents().getValue(), + runnerPost.getPostscriptContents().getValue(), runnerPost.getPullRequestUrl().getValue(), runnerPost.getDeadline().getValue(), runnerPost.getWatchedCount().getValue(), @@ -46,36 +50,6 @@ public static Detail of(final RunnerPost runnerPost, } } - public record DetailVersionTest(Long runnerPostId, - String title, - String contents, - String pullRequestUrl, - LocalDateTime deadline, - Integer watchedCount, - ReviewStatus reviewStatus, - RunnerResponse.Detail runnerProfile, - SupporterResponseTestVersion.Simple supporterProfile, - boolean isOwner, - List tags - ) { - - public static DetailVersionTest ofVersionTest(final RunnerPost runnerPost, final boolean isOwner) { - return new DetailVersionTest( - runnerPost.getId(), - runnerPost.getTitle().getValue(), - runnerPost.getContents().getValue(), - runnerPost.getPullRequestUrl().getValue(), - runnerPost.getDeadline().getValue(), - runnerPost.getWatchedCount().getValue(), - runnerPost.getReviewStatus(), - RunnerResponse.Detail.from(runnerPost.getRunner()), - SupporterResponseTestVersion.Simple.fromTestVersion(runnerPost.getSupporter()), - isOwner, - convertToTags(runnerPost) - ); - } - } - public record Simple(Long runnerPostId, String title, LocalDateTime deadline, @@ -140,6 +114,7 @@ public static Mine from(final RunnerPost runnerPost) { ); } } + public record SimpleInMyPage(Long runnerPostId, Long supporterId, String title, @@ -147,7 +122,8 @@ public record SimpleInMyPage(Long runnerPostId, List tags, int watchedCount, long applicantCount, - String reviewStatus + String reviewStatus, + boolean isReviewed ) { @@ -162,7 +138,8 @@ public static SimpleInMyPage from(final RunnerPost runnerPost, convertToTags(runnerPost), runnerPost.getWatchedCount().getValue(), applicantCount, - runnerPost.getReviewStatus().name() + runnerPost.getReviewStatus().name(), + runnerPost.getIsReviewed().getValue() ); } diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/repository/RunnerPostReadRepository.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/repository/RunnerPostReadRepository.java new file mode 100644 index 000000000..734b105df --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/repository/RunnerPostReadRepository.java @@ -0,0 +1,54 @@ +package touch.baton.domain.runnerpost.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.repository.dto.ApplicantCountDto; +import touch.baton.domain.runnerpost.repository.dto.ApplicantCountMappingDto; +import touch.baton.domain.runnerpost.vo.ReviewStatus; +import touch.baton.domain.tag.vo.TagReducedName; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public interface RunnerPostReadRepository extends JpaRepository { + + default ApplicantCountMappingDto findApplicantCountMappingByRunnerPostIds(final List runnerPostIds) { + final Map applicantCountMapping = countApplicantsByRunnerPostIds(runnerPostIds) + .stream() + .collect(Collectors.toMap( + ApplicantCountDto::runnerPostId, + ApplicantCountDto::applicantCount + )); + + return new ApplicantCountMappingDto(applicantCountMapping); + } + + @Query(value = """ + select new touch.baton.domain.runnerpost.repository.dto.ApplicantCountDto(rp.id, count(srp.id)) + from RunnerPost rp + left outer join fetch SupporterRunnerPost srp on srp.runnerPost.id = rp.id + where rp.id in :runnerPostIds + group by rp.id + """) + List countApplicantsByRunnerPostIds(@Param("runnerPostIds") final List runnerPostIds); + + @Query(""" + select rp + from RunnerPost rp + join fetch Runner r on r.id = rp.runner.id + join fetch Member m on m.id = r.member.id + join fetch RunnerPostTag rpt on rpt.runnerPost.id = rp.id + where rpt.tag.tagReducedName = :tagReducedName + and rp.reviewStatus = :reviewStatus + """) + Page findByTagReducedNameAndReviewStatus( + final Pageable pageable, + @Param("tagReducedName") final TagReducedName tagReducedName, + @Param("reviewStatus") final ReviewStatus reviewStatus); + +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/repository/RunnerPostRepository.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/repository/RunnerPostRepository.java index 3312a9e2c..e033a8ac1 100644 --- a/backend/baton/src/main/java/touch/baton/domain/runnerpost/repository/RunnerPostRepository.java +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/repository/RunnerPostRepository.java @@ -14,7 +14,7 @@ public interface RunnerPostRepository extends JpaRepository { @Query(value = """ - select rp + select rp, r, m from RunnerPost rp join fetch Runner r on r.id = rp.runner.id join fetch Member m on m.id = r.member.id @@ -22,7 +22,7 @@ public interface RunnerPostRepository extends JpaRepository { """) Optional joinMemberByRunnerPostId(@Param("runnerPostId") final Long runnerPostId); - Page findAll(final Pageable pageable); + Page findByReviewStatus(final Pageable pageable, final ReviewStatus reviewStatus); @Query(countQuery = """ select count(1) diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/repository/dto/ApplicantCountDto.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/repository/dto/ApplicantCountDto.java new file mode 100644 index 000000000..3191616c7 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/repository/dto/ApplicantCountDto.java @@ -0,0 +1,4 @@ +package touch.baton.domain.runnerpost.repository.dto; + +public record ApplicantCountDto(Long runnerPostId, Long applicantCount) { +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/repository/dto/ApplicantCountMappingDto.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/repository/dto/ApplicantCountMappingDto.java new file mode 100644 index 000000000..64d26021b --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/repository/dto/ApplicantCountMappingDto.java @@ -0,0 +1,10 @@ +package touch.baton.domain.runnerpost.repository.dto; + +import java.util.Map; + +public record ApplicantCountMappingDto(Map applicantCounts) { + + public Long getApplicantCountByRunnerPostId(final Long runnerPostId) { + return applicantCounts.get(runnerPostId); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/RunnerPostReadService.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/RunnerPostReadService.java new file mode 100644 index 000000000..13d6a27d1 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/RunnerPostReadService.java @@ -0,0 +1,35 @@ +package touch.baton.domain.runnerpost.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.repository.RunnerPostReadRepository; +import touch.baton.domain.runnerpost.repository.dto.ApplicantCountMappingDto; +import touch.baton.domain.runnerpost.vo.ReviewStatus; +import touch.baton.domain.tag.vo.TagReducedName; + +import java.util.List; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class RunnerPostReadService { + + private final RunnerPostReadRepository runnerPostReadRepository; + + public Page readRunnerPostByTagNameAndReviewStatus(final Pageable pageable, + final String tagName, + final ReviewStatus reviewStatus + ) { + final TagReducedName tagReducedName = TagReducedName.from(tagName); + + return runnerPostReadRepository.findByTagReducedNameAndReviewStatus(pageable, tagReducedName, reviewStatus); + } + + public ApplicantCountMappingDto readApplicantCountMappingByRunnerPostIds(final List runnerPostIds) { + return runnerPostReadRepository.findApplicantCountMappingByRunnerPostIds(runnerPostIds); + } +} 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 4390136ff..5acbf99d3 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 @@ -58,7 +58,9 @@ public Long createRunnerPost(final Runner runner, final RunnerPostCreateRequest private RunnerPost toDomain(final Runner runner, final RunnerPostCreateRequest request) { return RunnerPost.newInstance(request.title(), - request.contents(), + request.implementedContents(), + request.curiousContents(), + request.postscriptContents(), request.pullRequestUrl(), request.deadline(), runner); @@ -144,8 +146,8 @@ public Long createRunnerPostApplicant(final Supporter supporter, return supporterRunnerPostRepository.save(runnerPostApplicant).getId(); } - public Page readAllRunnerPosts(final Pageable pageable) { - return runnerPostRepository.findAll(pageable); + public Page readRunnerPostsByReviewStatus(final Pageable pageable, final ReviewStatus reviewStatus) { + return runnerPostRepository.findByReviewStatus(pageable, reviewStatus); } public List readRunnerPostsByRunnerId(final Long runnerId) { diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/dto/RunnerPostCreateRequest.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/dto/RunnerPostCreateRequest.java index c2fa44f1d..52f5d3e3b 100644 --- a/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/dto/RunnerPostCreateRequest.java +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/dto/RunnerPostCreateRequest.java @@ -8,14 +8,7 @@ import java.time.LocalDateTime; import java.util.List; -import static touch.baton.domain.common.exception.ClientErrorCode.CONTENTS_ARE_NULL; -import static touch.baton.domain.common.exception.ClientErrorCode.CONTENTS_OVERFLOW; -import static touch.baton.domain.common.exception.ClientErrorCode.DEADLINE_IS_NULL; -import static touch.baton.domain.common.exception.ClientErrorCode.PAST_DEADLINE; -import static touch.baton.domain.common.exception.ClientErrorCode.PULL_REQUEST_URL_IS_NOT_URL; -import static touch.baton.domain.common.exception.ClientErrorCode.PULL_REQUEST_URL_IS_NULL; -import static touch.baton.domain.common.exception.ClientErrorCode.TAGS_ARE_NULL; -import static touch.baton.domain.common.exception.ClientErrorCode.TITLE_IS_NULL; +import static touch.baton.domain.common.exception.ClientErrorCode.*; public record RunnerPostCreateRequest(@ValidNotNull(clientErrorCode = TITLE_IS_NULL) String title, @@ -27,8 +20,14 @@ public record RunnerPostCreateRequest(@ValidNotNull(clientErrorCode = TITLE_IS_N @ValidNotNull(clientErrorCode = DEADLINE_IS_NULL) @ValidFuture(clientErrorCode = PAST_DEADLINE) LocalDateTime deadline, - @ValidNotNull(clientErrorCode = CONTENTS_ARE_NULL) + @ValidNotNull(clientErrorCode = IMPLEMENTED_CONTENTS_ARE_NULL) @ValidMaxLength(clientErrorCode = CONTENTS_OVERFLOW, max = 1000) - String contents + String implementedContents, + @ValidNotNull(clientErrorCode = CURIOUS_CONTENTS_ARE_NULL) + @ValidMaxLength(clientErrorCode = CONTENTS_OVERFLOW, max = 1000) + String curiousContents, + @ValidNotNull(clientErrorCode = POSTSCRIPT_CONTENTS_ARE_NULL) + @ValidMaxLength(clientErrorCode = CONTENTS_OVERFLOW, max = 1000) + String postscriptContents ) { } 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 5a17a8a99..2c2720038 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 @@ -2,29 +2,9 @@ import touch.baton.domain.common.exception.ClientErrorCode; import touch.baton.domain.common.exception.validator.ValidNotNull; -import touch.baton.domain.runnerpost.exception.validator.ValidFuture; -import touch.baton.domain.runnerpost.exception.validator.ValidMaxLength; - -import java.time.LocalDateTime; -import java.util.List; 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/runnerpost/vo/CuriousContents.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/CuriousContents.java new file mode 100644 index 000000000..0cb2ebc19 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/CuriousContents.java @@ -0,0 +1,32 @@ +package touch.baton.domain.runnerpost.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class CuriousContents { + + @Column(name = "curious_contents", nullable = false, columnDefinition = "TEXT") + private String value; + + public CuriousContents(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("CuriousContents 객체 내부에 value 는 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/ImplementedContents.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/ImplementedContents.java new file mode 100644 index 000000000..09abd6149 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/ImplementedContents.java @@ -0,0 +1,32 @@ +package touch.baton.domain.runnerpost.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class ImplementedContents { + + @Column(name = "implemented_contents", nullable = false, columnDefinition = "TEXT") + private String value; + + public ImplementedContents(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("ImplementedContents 객체 내부에 value 는 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/IsReviewed.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/IsReviewed.java new file mode 100644 index 000000000..e96528fa0 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/IsReviewed.java @@ -0,0 +1,35 @@ +package touch.baton.domain.runnerpost.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnDefault; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class IsReviewed { + + @ColumnDefault(value = "false") + @Column(name = "is_reviewed", nullable = false) + private boolean value = false; + + private IsReviewed(final boolean value) { + this.value = value; + } + + public static IsReviewed notReviewed() { + return new IsReviewed(false); + } + + public static IsReviewed reviewed() { + return new IsReviewed(true); + } + + public boolean getValue() { + return value; + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/PostscriptContents.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/PostscriptContents.java new file mode 100644 index 000000000..b98c64089 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/PostscriptContents.java @@ -0,0 +1,32 @@ +package touch.baton.domain.runnerpost.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class PostscriptContents { + + @Column(name = "postscript_contents", nullable = false, columnDefinition = "TEXT") + private String value; + + public PostscriptContents(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("PostscriptContents 객체 내부에 value 는 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/RunnerPostTag.java b/backend/baton/src/main/java/touch/baton/domain/tag/RunnerPostTag.java index 2c0c6d8af..a496e7571 100644 --- a/backend/baton/src/main/java/touch/baton/domain/tag/RunnerPostTag.java +++ b/backend/baton/src/main/java/touch/baton/domain/tag/RunnerPostTag.java @@ -58,8 +58,4 @@ private void validateNotNull(final RunnerPost runnerPost, final Tag tag) { throw new RunnerPostTagDomainException("RunnerPostTag 의 tag 는 null 일 수 없습니다."); } } - - public boolean isSameTagName(final String tagName) { - return tag.isSameTagName(tagName); - } } diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/RunnerPostTags.java b/backend/baton/src/main/java/touch/baton/domain/tag/RunnerPostTags.java index 52dd9e5e7..abadc38c1 100644 --- a/backend/baton/src/main/java/touch/baton/domain/tag/RunnerPostTags.java +++ b/backend/baton/src/main/java/touch/baton/domain/tag/RunnerPostTags.java @@ -4,6 +4,7 @@ import jakarta.persistence.OneToMany; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.BatchSize; import java.util.ArrayList; import java.util.List; @@ -17,6 +18,7 @@ @Embeddable public class RunnerPostTags { + @BatchSize(size = 5) @OneToMany(mappedBy = "runnerPost", cascade = PERSIST, orphanRemoval = true) private List runnerPostTags = new ArrayList<>(); diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/Tag.java b/backend/baton/src/main/java/touch/baton/domain/tag/Tag.java index bce6189f5..bc15db73a 100644 --- a/backend/baton/src/main/java/touch/baton/domain/tag/Tag.java +++ b/backend/baton/src/main/java/touch/baton/domain/tag/Tag.java @@ -1,5 +1,6 @@ package touch.baton.domain.tag; +import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -9,6 +10,7 @@ import lombok.NoArgsConstructor; import touch.baton.domain.common.vo.TagName; import touch.baton.domain.tag.exception.TagDomainException; +import touch.baton.domain.tag.vo.TagReducedName; import java.util.Objects; @@ -27,30 +29,34 @@ public class Tag { @Embedded private TagName tagName; + @Embedded + private TagReducedName tagReducedName; + @Builder - private Tag(final TagName tagName) { - this(null, tagName); + private Tag(final TagName tagName, final TagReducedName tagReducedName) { + this(null, tagName, tagReducedName); } - private Tag(final Long id, final TagName tagName) { - validateNotNull(tagName); + private Tag(final Long id, final TagName tagName, final TagReducedName tagReducedName) { + validateNotNull(tagName, tagReducedName); this.id = id; this.tagName = tagName; + this.tagReducedName = tagReducedName; } - private void validateNotNull(final TagName tagName) { + private void validateNotNull(final TagName tagName, final TagReducedName tagReducedName) { if (Objects.isNull(tagName)) { throw new TagDomainException("Tag 의 tagName 은 null 일 수 없습니다."); } + if (Objects.isNull(tagReducedName)) { + throw new TagDomainException("Tag 의 tagReducedName 은 null 일 수 없습니다."); + } } public static Tag newInstance(final String tagName) { return Tag.builder() .tagName(new TagName(tagName)) + .tagReducedName(TagReducedName.from(tagName)) .build(); } - - public boolean isSameTagName(final String tagName) { - return this.tagName.equals(new TagName(tagName)); - } } diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/controller/TagController.java b/backend/baton/src/main/java/touch/baton/domain/tag/controller/TagController.java new file mode 100644 index 000000000..2aa05d1c0 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/controller/TagController.java @@ -0,0 +1,36 @@ +package touch.baton.domain.tag.controller; + +import jakarta.annotation.Nullable; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import touch.baton.domain.tag.controller.response.TagSearchResponse; +import touch.baton.domain.tag.controller.response.TagSearchResponses; +import touch.baton.domain.tag.service.TagService; + +import java.util.Collections; +import java.util.List; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/tags") +@RestController +public class TagController { + + private final TagService tagService; + + @GetMapping("/search") + public ResponseEntity readTagsByTagName(@Nullable @RequestParam(required = false) final String tagName) { + if (tagName == null || tagName.isBlank()) { + return ResponseEntity.ok().body(TagSearchResponses.Detail.from(Collections.emptyList())); + } + + final List tagSearchResponses = tagService.readTagsByReducedName(tagName).stream() + .map(TagSearchResponse.TagResponse::from) + .toList(); + + return ResponseEntity.ok(TagSearchResponses.Detail.from(tagSearchResponses)); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/controller/response/TagSearchResponse.java b/backend/baton/src/main/java/touch/baton/domain/tag/controller/response/TagSearchResponse.java new file mode 100644 index 000000000..f91aa19dc --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/controller/response/TagSearchResponse.java @@ -0,0 +1,12 @@ +package touch.baton.domain.tag.controller.response; + +import touch.baton.domain.tag.Tag; + +public record TagSearchResponse() { + + public record TagResponse(Long id, String tagName) { + public static TagResponse from(final Tag tag) { + return new TagResponse(tag.getId(), tag.getTagName().getValue()); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/controller/response/TagSearchResponses.java b/backend/baton/src/main/java/touch/baton/domain/tag/controller/response/TagSearchResponses.java new file mode 100644 index 000000000..6ff10f2ca --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/controller/response/TagSearchResponses.java @@ -0,0 +1,12 @@ +package touch.baton.domain.tag.controller.response; + +import java.util.List; + +public record TagSearchResponses() { + + public record Detail(List data) { + public static Detail from(final List data) { + return new Detail(data); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/repository/TagRepository.java b/backend/baton/src/main/java/touch/baton/domain/tag/repository/TagRepository.java index 958e52954..2ed88778e 100644 --- a/backend/baton/src/main/java/touch/baton/domain/tag/repository/TagRepository.java +++ b/backend/baton/src/main/java/touch/baton/domain/tag/repository/TagRepository.java @@ -1,12 +1,26 @@ package touch.baton.domain.tag.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import touch.baton.domain.common.vo.TagName; import touch.baton.domain.tag.Tag; +import touch.baton.domain.tag.vo.TagReducedName; +import java.util.List; import java.util.Optional; public interface TagRepository extends JpaRepository { Optional findByTagName(final TagName tagName); + + @Query(""" + select t + from Tag t + where t.tagReducedName like :tagReducedName% + order by t.tagReducedName asc + limit 10 + """) + List readTagsByReducedName(@Param("tagReducedName")TagReducedName tagReducedName); + } diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/service/TagService.java b/backend/baton/src/main/java/touch/baton/domain/tag/service/TagService.java new file mode 100644 index 000000000..2b6a2163b --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/service/TagService.java @@ -0,0 +1,24 @@ +package touch.baton.domain.tag.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import touch.baton.domain.tag.Tag; +import touch.baton.domain.tag.repository.TagRepository; +import touch.baton.domain.tag.vo.TagReducedName; + +import java.util.List; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class TagService { + + private final TagRepository tagRepository; + + public List readTagsByReducedName(final String tagName) { + final String reducedName = TagReducedName.from(tagName).getValue(); + + return tagRepository.readTagsByReducedName(TagReducedName.from(tagName)); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/vo/TagReducedName.java b/backend/baton/src/main/java/touch/baton/domain/tag/vo/TagReducedName.java new file mode 100644 index 000000000..acf7f1074 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/vo/TagReducedName.java @@ -0,0 +1,46 @@ +package touch.baton.domain.tag.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class TagReducedName { + + private static final String BLANK = " "; + private static final String NOT_BLANK = ""; + + @Column(name = "reduced_name", nullable = false) + private String value; + + private TagReducedName(final String value) { + this.value = value; + } + + public static TagReducedName from(final String notReducedValue) { + validateNotNull(notReducedValue); + final String reducedValue = reduceName(notReducedValue); + return new TagReducedName(reducedValue); + } + + private static void validateNotNull(final String notReducedValue) { + if (Objects.isNull(notReducedValue)) { + throw new IllegalArgumentException("TagReducedName 객체를 생성할 때 notReducedValue 은 null 일 수 없습니다."); + } + } + + private static String reduceName(final String beforeReduced) { + final String afterLowerCase = beforeReduced.toLowerCase(); + final String afterReduceBlank = afterLowerCase.replaceAll(BLANK, NOT_BLANK); + return afterReduceBlank; + } +} diff --git a/backend/baton/src/main/java/touch/baton/infra/auth/jwt/JwtConfig.java b/backend/baton/src/main/java/touch/baton/infra/auth/jwt/JwtConfig.java index 49bfcf2fc..eadf30fce 100644 --- a/backend/baton/src/main/java/touch/baton/infra/auth/jwt/JwtConfig.java +++ b/backend/baton/src/main/java/touch/baton/infra/auth/jwt/JwtConfig.java @@ -3,17 +3,20 @@ import io.jsonwebtoken.security.Keys; import lombok.RequiredArgsConstructor; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Profile; import java.security.Key; import static java.nio.charset.StandardCharsets.UTF_8; +@Profile("!test") @RequiredArgsConstructor @ConfigurationProperties("jwt.token") public class JwtConfig { private final String secretKey; private final String issuer; + private final int expireMinutes; public Key getSecretKey() { return Keys.hmacShaKeyFor(secretKey.getBytes(UTF_8)); @@ -22,4 +25,8 @@ public Key getSecretKey() { public String getIssuer() { return this.issuer; } + + public int getExpireMinutes() { + return expireMinutes; + } } diff --git a/backend/baton/src/main/java/touch/baton/infra/auth/jwt/JwtDecoder.java b/backend/baton/src/main/java/touch/baton/infra/auth/jwt/JwtDecoder.java index 7d64c6c66..ab1870e8a 100644 --- a/backend/baton/src/main/java/touch/baton/infra/auth/jwt/JwtDecoder.java +++ b/backend/baton/src/main/java/touch/baton/infra/auth/jwt/JwtDecoder.java @@ -1,6 +1,7 @@ package touch.baton.infra.auth.jwt; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.IncorrectClaimException; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; @@ -8,30 +9,56 @@ import io.jsonwebtoken.MissingClaimException; import io.jsonwebtoken.security.SignatureException; import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.oauth.AuthorizationHeader; import touch.baton.domain.oauth.exception.OauthRequestException; +@Profile("!test") @RequiredArgsConstructor @Component public class JwtDecoder { private final JwtConfig jwtConfig; - public Claims parseJwtToken(final String token) { + public Claims parseAuthorizationHeader(final AuthorizationHeader authorizationHeader) { try { final JwtParser jwtParser = Jwts.parserBuilder() .setSigningKey(jwtConfig.getSecretKey()) .requireIssuer(jwtConfig.getIssuer()) .build(); + final String token = authorizationHeader.parseBearerAccessToken(); return jwtParser.parseClaimsJws(token).getBody(); - } catch (SignatureException e) { + } catch (final SignatureException e) { throw new OauthRequestException(ClientErrorCode.JWT_SIGNATURE_IS_WRONG); - } catch (MalformedJwtException e) { + } catch (final ExpiredJwtException e) { + throw new OauthRequestException(ClientErrorCode.JWT_CLAIM_IS_ALREADY_EXPIRED); + } catch (final MalformedJwtException e) { throw new OauthRequestException(ClientErrorCode.JWT_FORM_IS_WRONG); - } catch (MissingClaimException | IncorrectClaimException e) { + } catch (final MissingClaimException | IncorrectClaimException e) { throw new OauthRequestException(ClientErrorCode.JWT_CLAIM_IS_WRONG); } } + + public Claims parseExpiredAuthorizationHeader(final AuthorizationHeader authorizationHeader) { + try { + final JwtParser jwtParser = Jwts.parserBuilder() + .setSigningKey(jwtConfig.getSecretKey()) + .requireIssuer(jwtConfig.getIssuer()) + .build(); + + final String token = authorizationHeader.parseBearerAccessToken(); + return jwtParser.parseClaimsJws(token).getBody(); + } catch (final SignatureException e) { + throw new OauthRequestException(ClientErrorCode.JWT_SIGNATURE_IS_WRONG); + } catch (final MalformedJwtException e) { + throw new OauthRequestException(ClientErrorCode.JWT_FORM_IS_WRONG); + } catch (final MissingClaimException | IncorrectClaimException e) { + throw new OauthRequestException(ClientErrorCode.JWT_CLAIM_IS_WRONG); + } catch (final ExpiredJwtException e) { + return e.getClaims(); + } + } } diff --git a/backend/baton/src/main/java/touch/baton/infra/auth/jwt/JwtEncoder.java b/backend/baton/src/main/java/touch/baton/infra/auth/jwt/JwtEncoder.java index a351c2e88..284122c40 100644 --- a/backend/baton/src/main/java/touch/baton/infra/auth/jwt/JwtEncoder.java +++ b/backend/baton/src/main/java/touch/baton/infra/auth/jwt/JwtEncoder.java @@ -1,17 +1,18 @@ package touch.baton.infra.auth.jwt; - import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import java.time.Duration; import java.util.Date; import java.util.Map; +@Profile("!test") @RequiredArgsConstructor @Component public class JwtEncoder { @@ -20,7 +21,7 @@ public class JwtEncoder { public String jwtToken(final Map payload) { final Date now = new Date(); - final Date expiration = new Date(now.getTime() + Duration.ofDays(30).toMillis()); + final Date expiration = new Date(now.getTime() + Duration.ofMinutes(jwtConfig.getExpireMinutes()).toMillis()); final Claims claims = Jwts.claims(); final JwtBuilder jwtBuilder = Jwts.builder() diff --git a/backend/baton/src/main/java/touch/baton/infra/auth/oauth/github/response/GithubMemberResponse.java b/backend/baton/src/main/java/touch/baton/infra/auth/oauth/github/response/GithubMemberResponse.java index a0012f1b9..05e5001c7 100644 --- a/backend/baton/src/main/java/touch/baton/infra/auth/oauth/github/response/GithubMemberResponse.java +++ b/backend/baton/src/main/java/touch/baton/infra/auth/oauth/github/response/GithubMemberResponse.java @@ -7,7 +7,7 @@ import touch.baton.domain.member.vo.OauthId; import touch.baton.domain.member.vo.SocialId; import touch.baton.domain.oauth.OauthInformation; -import touch.baton.domain.oauth.SocialToken; +import touch.baton.domain.oauth.token.SocialToken; import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; diff --git a/backend/baton/src/main/java/touch/baton/infra/exception/InfraException.java b/backend/baton/src/main/java/touch/baton/infra/exception/InfraException.java new file mode 100644 index 000000000..6ae8d8651 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/infra/exception/InfraException.java @@ -0,0 +1,10 @@ +package touch.baton.infra.exception; + +import touch.baton.domain.common.exception.BaseException; + +public class InfraException extends BaseException { + + public InfraException(final String message) { + super(message); + } +} diff --git a/backend/baton/src/main/java/touch/baton/infra/github/GithubBranchManager.java b/backend/baton/src/main/java/touch/baton/infra/github/GithubBranchManager.java new file mode 100644 index 000000000..8d86a1066 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/infra/github/GithubBranchManager.java @@ -0,0 +1,87 @@ +package touch.baton.infra.github; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestTemplate; +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.ClientRequestException; +import touch.baton.domain.member.service.dto.GithubBranchManageable; +import touch.baton.infra.exception.InfraException; +import touch.baton.infra.github.request.CreateBranchRequest; +import touch.baton.infra.github.response.ReadBranchInfoResponse; + +@Profile("!test") +@Component +public class GithubBranchManager implements GithubBranchManageable { + + private static final String GITHUB_API_URL = "https://api.github.com/repos/baton-mission/"; + private static final String CREATE_BRANCH_API_POSTFIX = "/git/refs"; + private static final String NEW_BRANCH_HEAD_PREFIX = "refs/heads/"; + private static final String READ_BRANCH_API_POSTFIX = "/git/refs/heads/main"; + private static final String DUPLICATED_BRANCH_ERROR_MESSAGE = "Reference already exists"; + + private final String token; + + public GithubBranchManager(@Value("${github.personal_access_token}") final String token) { + this.token = token; + } + + @Override + public void createBranch(final String repoName, final String newBranchName) { + final RestTemplate restTemplate = new RestTemplate(); + final ReadBranchInfoResponse branchInfoResponse = readMainBranch(repoName); + + final HttpHeaders httpHeaders = setBearerAuth(); + final String requestUrl = GITHUB_API_URL + repoName + CREATE_BRANCH_API_POSTFIX; + final String ref = NEW_BRANCH_HEAD_PREFIX + newBranchName; + final String sha = branchInfoResponse.object().sha(); + final CreateBranchRequest createBranchRequest = new CreateBranchRequest(ref, sha); + final HttpEntity entity = new HttpEntity<>(createBranchRequest, httpHeaders); + try { + restTemplate.exchange(requestUrl, HttpMethod.POST, entity, String.class); + } catch (HttpStatusCodeException e) { + handleCreateBranch(e.getResponseBodyAsString()); + throw new InfraException("브랜치 추가가 올바르게 이루어지지 않았습니다. github 응답: " + e.getResponseBodyAsString()); + } + } + + private HttpHeaders setBearerAuth() { + final HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(token); + return headers; + } + + private void handleCreateBranch(final String errorBodyMessage) { + if (errorBodyMessage.contains(DUPLICATED_BRANCH_ERROR_MESSAGE)) { + throw new ClientRequestException(ClientErrorCode.DUPLICATED_BRANCH_NAME); + } + } + + private ReadBranchInfoResponse readMainBranch(final String repoName) { + final RestTemplate restTemplate = new RestTemplate(); + final String requestUrl = GITHUB_API_URL + repoName + READ_BRANCH_API_POSTFIX; + final HttpHeaders httpHeaders = setBearerAuth(); + final HttpEntity entity = new HttpEntity<>(httpHeaders); + try { + final ResponseEntity response = restTemplate.exchange(requestUrl, HttpMethod.GET, entity, ReadBranchInfoResponse.class); + return response.getBody(); + } catch (HttpStatusCodeException e) { + handleReadBranch(e.getStatusCode()); + throw new InfraException("레포지토리 브랜치 조회가 올바르게 이루어지지 않았습니다. github 응답: " + e.getResponseBodyAsString()); + } + } + + private void handleReadBranch(final HttpStatusCode statusCode) { + if (statusCode == HttpStatus.NOT_FOUND) { + throw new ClientRequestException(ClientErrorCode.REPO_NOT_FOUND); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/infra/github/request/CreateBranchRequest.java b/backend/baton/src/main/java/touch/baton/infra/github/request/CreateBranchRequest.java new file mode 100644 index 000000000..eac164d01 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/infra/github/request/CreateBranchRequest.java @@ -0,0 +1,4 @@ +package touch.baton.infra.github.request; + +public record CreateBranchRequest(String ref, String sha) { +} diff --git a/backend/baton/src/main/java/touch/baton/infra/github/response/ReadBranchInfoResponse.java b/backend/baton/src/main/java/touch/baton/infra/github/response/ReadBranchInfoResponse.java new file mode 100644 index 000000000..9dc440e34 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/infra/github/response/ReadBranchInfoResponse.java @@ -0,0 +1,4 @@ +package touch.baton.infra.github.response; + +public record ReadBranchInfoResponse(String ref, String nodeId, String url, ReadLastCommitInfoResponse object) { +} diff --git a/backend/baton/src/main/java/touch/baton/infra/github/response/ReadLastCommitInfoResponse.java b/backend/baton/src/main/java/touch/baton/infra/github/response/ReadLastCommitInfoResponse.java new file mode 100644 index 000000000..afbb84d5e --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/infra/github/response/ReadLastCommitInfoResponse.java @@ -0,0 +1,4 @@ +package touch.baton.infra.github.response; + +public record ReadLastCommitInfoResponse(String sha, String type, String url) { +} diff --git a/backend/baton/src/main/resources/application.yml b/backend/baton/src/main/resources/application.yml index 8e89b7285..ddb45ab4b 100644 --- a/backend/baton/src/main/resources/application.yml +++ b/backend/baton/src/main/resources/application.yml @@ -1,12 +1,13 @@ spring: flyway: - enabled: false + enabled: true + baseline-on-migrate: true jpa: properties: hibernate: format_sql: true hibernate: - ddl-auto: create + ddl-auto: validate data: web: @@ -27,6 +28,9 @@ oauth: client_secret: ${OAUTH_GITHUB_CLIENT_SECRET} scope: ${OAUTH_GITHUB_SCOPE} +github: + personal_access_token: ${PERSONAL_MISSION_ACCESS_TOKEN} + cors: allowed-origin: http://localhost:3000 @@ -34,3 +38,7 @@ jwt: token: secret_key: ${JWT_SECRET_KEY} issuer: ${JWT_ISSUER} + expire_minutes: ${JWT_EXPIRE_MINUTES} + +refresh_token: + expire_minutes: ${REFRESH_TOKEN_MINUTES} diff --git a/backend/baton/src/main/resources/db/migration/V0__init.sql b/backend/baton/src/main/resources/db/migration/V0__init.sql new file mode 100644 index 000000000..cb9d68feb --- /dev/null +++ b/backend/baton/src/main/resources/db/migration/V0__init.sql @@ -0,0 +1,159 @@ +create table member +( + id bigint auto_increment + primary key, + name varchar(255) not null, + social_id varchar(255) not null, + oauth_id varchar(255) not null, + github_url varchar(255) not null, + image_url varchar(255) not null, + company varchar(255) not null, + created_at datetime(6) not null, + updated_at datetime(6) not null, + deleted_at datetime(6) null +); + +create table runner +( + id bigint auto_increment + primary key, + introduction varchar(255) not null, + created_at datetime(6) not null, + updated_at datetime(6) not null, + deleted_at datetime(6) null, + member_id bigint not null, + constraint uk_runner_in_member_id + unique (member_id), + constraint fk_runner_to_member + foreign key (member_id) references member (id) +); + +create table supporter +( + id bigint auto_increment + primary key, + review_count int default 0 null, + introduction varchar(255) not null, + created_at datetime(6) not null, + updated_at datetime(6) not null, + deleted_at datetime(6) null, + member_id bigint not null, + constraint uk_supporter_in_member_id + unique (member_id), + constraint fk_supporter_to_member + foreign key (member_id) references member (id) +); + +create table runner_post +( + id bigint auto_increment + primary key, + title varchar(255) not null, + contents text not null, + pull_request_url varchar(2083) not null, + deadline datetime(6) not null, + watch_count int default 0 not null, + review_status enum ('DONE', 'IN_PROGRESS', 'NOT_STARTED', 'OVERDUE') not null, + created_at datetime(6) not null, + updated_at datetime(6) not null, + deleted_at datetime(6) null, + runner_id bigint not null, + supporter_id bigint null, + constraint fk_runner_post_to_runner + foreign key (runner_id) references runner (id), + constraint fk_runner_post_to_supporter + foreign key (supporter_id) references supporter (id) +); + +create table supporter_feedback +( + id bigint auto_increment + primary key, + review_type enum ('BAD', 'GOOD', 'GREAT') not null, + description text null, + created_at datetime(6) not null, + updated_at datetime(6) not null, + deleted_at datetime(6) null, + supporter_id bigint not null, + runner_id bigint not null, + runner_post_id bigint not null, + constraint uk_supporter_feedback_in_runner_post_id + unique (runner_post_id), + constraint fk_supporter_feed_back_to_runner + foreign key (runner_id) references runner (id), + constraint fk_supporter_feed_back_to_runner_post + foreign key (runner_post_id) references runner_post (id), + constraint fk_supporter_feed_back_to_supporter + foreign key (supporter_id) references supporter (id) +); + +create table supporter_runner_post +( + id bigint auto_increment + primary key, + message varchar(255) not null, + created_at datetime(6) not null, + updated_at datetime(6) not null, + deleted_at datetime(6) null, + supporter_id bigint not null, + runner_post_id bigint not null, + constraint fk_support_runner_post_to_runner_post + foreign key (runner_post_id) references runner_post (id), + constraint fk_support_runner_post_to_supporter + foreign key (supporter_id) references supporter (id) +); + +create table tag +( + id bigint auto_increment + primary key, + name varchar(255) not null, + reduced_name varchar(255) not null, + constraint uk_tag_in_name + unique (name) +); + +create table runner_post_tag +( + id bigint auto_increment + primary key, + runner_post_id bigint not null, + tag_id bigint not null, + constraint fk_runner_post_tag_to_runner_post + foreign key (runner_post_id) references runner_post (id), + constraint fk_runner_post_tag_to_tag + foreign key (tag_id) references tag (id) +); + +create table technical_tag +( + id bigint auto_increment + primary key, + name varchar(255) not null, + constraint uk_technical_tag_in_name + unique (name) +); + +create table runner_technical_tag +( + id bigint auto_increment + primary key, + runner_id bigint not null, + technical_tag_id bigint not null, + constraint fk_runner_technical_tag_to_runner + foreign key (runner_id) references runner (id), + constraint fk_runner_technical_tag_to_technical_tag + foreign key (technical_tag_id) references technical_tag (id) +); + +create table supporter_technical_tag +( + id bigint auto_increment + primary key, + supporter_id bigint not null, + technical_tag_id bigint not null, + constraint fk_supporter_technical_tag_to_supporter + foreign key (supporter_id) references supporter (id), + constraint fk_supporter_technical_tag_to_technical_tag + foreign key (technical_tag_id) references technical_tag (id) +); diff --git a/backend/baton/src/main/resources/db/migration/V20230910_1__add_new_contents_columns.sql b/backend/baton/src/main/resources/db/migration/V20230910_1__add_new_contents_columns.sql new file mode 100644 index 000000000..a05dd1733 --- /dev/null +++ b/backend/baton/src/main/resources/db/migration/V20230910_1__add_new_contents_columns.sql @@ -0,0 +1,3 @@ +ALTER TABLE runner_post ADD COLUMN implemented_contents TEXT AFTER title; +ALTER TABLE runner_post ADD COLUMN curious_contents TEXT AFTER implemented_contents; +ALTER TABLE runner_post ADD COLUMN postscript_contents TEXT AFTER curious_contents; diff --git a/backend/baton/src/main/resources/db/migration/V20230910_2__set_contents_data_to_new_contents.sql b/backend/baton/src/main/resources/db/migration/V20230910_2__set_contents_data_to_new_contents.sql new file mode 100644 index 000000000..9aa64bd21 --- /dev/null +++ b/backend/baton/src/main/resources/db/migration/V20230910_2__set_contents_data_to_new_contents.sql @@ -0,0 +1,7 @@ +UPDATE runner_post SET implemented_contents = contents; +UPDATE runner_post SET curious_contents = ''; +UPDATE runner_post SET postscript_contents = ''; + +ALTER TABLE runner_post MODIFY implemented_contents TEXT NOT NULL; +ALTER TABLE runner_post MODIFY curious_contents TEXT NOT NULL; +ALTER TABLE runner_post MODIFY postscript_contents TEXT NOT NULL; diff --git a/backend/baton/src/main/resources/db/migration/V20230910_3__drop_column_of_contents.sql b/backend/baton/src/main/resources/db/migration/V20230910_3__drop_column_of_contents.sql new file mode 100644 index 000000000..3ca016fcc --- /dev/null +++ b/backend/baton/src/main/resources/db/migration/V20230910_3__drop_column_of_contents.sql @@ -0,0 +1 @@ +ALTER TABLE runner_post DROP COLUMN contents; diff --git a/backend/baton/src/main/resources/db/migration/V20230913__alter_tag_name_column_utf8_bin.sql b/backend/baton/src/main/resources/db/migration/V20230913__alter_tag_name_column_utf8_bin.sql new file mode 100644 index 000000000..f2f924127 --- /dev/null +++ b/backend/baton/src/main/resources/db/migration/V20230913__alter_tag_name_column_utf8_bin.sql @@ -0,0 +1,2 @@ +ALTER TABLE tag CHANGE name name VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_bin; +ALTER TABLE technical_tag CHANGE name name VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_bin; diff --git a/backend/baton/src/main/resources/db/migration/V20230914__create_refresh_token_table.sql b/backend/baton/src/main/resources/db/migration/V20230914__create_refresh_token_table.sql new file mode 100644 index 000000000..97d3d9881 --- /dev/null +++ b/backend/baton/src/main/resources/db/migration/V20230914__create_refresh_token_table.sql @@ -0,0 +1,15 @@ +CREATE TABLE refresh_token +( + id BIGINT AUTO_INCREMENT, + expire_date TIMESTAMP(6) NOT NULL, + token TEXT NOT NULL, + created_at TIMESTAMP(6) NOT NULL, + updated_at TIMESTAMP(6) NOT NULL, + deleted_at TIMESTAMP(6), + member_id BIGINT NOT NULL UNIQUE, + PRIMARY KEY (id) +); + +ALTER TABLE refresh_token + ADD CONSTRAINT fk_refresh_token_to_member + FOREIGN KEY (member_id) REFERENCES member (id); diff --git a/backend/baton/src/main/resources/db/migration/V20230920_1__add_new_runner_post_is_reviewed_column.sql b/backend/baton/src/main/resources/db/migration/V20230920_1__add_new_runner_post_is_reviewed_column.sql new file mode 100644 index 000000000..3de6086a0 --- /dev/null +++ b/backend/baton/src/main/resources/db/migration/V20230920_1__add_new_runner_post_is_reviewed_column.sql @@ -0,0 +1 @@ +ALTER TABLE runner_post ADD COLUMN is_reviewed BOOLEAN DEFAULT FALSE; diff --git a/backend/baton/src/main/resources/db/migration/V20230920_2__alter_runner_post_watched_count_column_name.sql b/backend/baton/src/main/resources/db/migration/V20230920_2__alter_runner_post_watched_count_column_name.sql new file mode 100644 index 000000000..8d240ccf0 --- /dev/null +++ b/backend/baton/src/main/resources/db/migration/V20230920_2__alter_runner_post_watched_count_column_name.sql @@ -0,0 +1 @@ +ALTER TABLE runner_post CHANGE COLUMN watch_count watched_count INT DEFAULT 0 NOT NULL; 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 b6a60dd32..ef53e2a55 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 @@ -4,27 +4,22 @@ import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; -import java.util.Map; - import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; public class AssuredSupport { - public static ExtractableResponse post(final String uri, final Object params) { + public static ExtractableResponse post(final String uri) { return RestAssured .given().log().ifValidationFails() - .contentType(APPLICATION_JSON_VALUE) - .body(params) .when().log().ifValidationFails() .post(uri) .then().log().ifError() .extract(); } - public static ExtractableResponse post(final String uri, final String accessToken, final Object body) { + public static ExtractableResponse post(final String uri, final Object body) { return RestAssured .given().log().ifValidationFails() - .auth().preemptive().oauth2(accessToken) .contentType(APPLICATION_JSON_VALUE) .body(body) .when().log().ifValidationFails() @@ -33,163 +28,167 @@ public static ExtractableResponse post(final String uri, final String .extract(); } - public static ExtractableResponse post(final String uri, - final String accessToken, - final Map pathVariables, - final Object requestBody - ) { + public static ExtractableResponse post(final String uri, final String accessToken) { return RestAssured .given().log().ifValidationFails() .auth().preemptive().oauth2(accessToken) - .contentType(APPLICATION_JSON_VALUE) - .pathParams(pathVariables) - .body(requestBody) .when().log().ifValidationFails() .post(uri) .then().log().ifError() .extract(); } - public static ExtractableResponse post(final String uri, final Object params, final String accessToken) { + public static ExtractableResponse post(final String uri, final String accessToken, final String refreshToken) { return RestAssured .given().log().ifValidationFails() .auth().preemptive().oauth2(accessToken) - .contentType(APPLICATION_JSON_VALUE) - .body(params) + .cookie("refreshToken", refreshToken) .when().log().ifValidationFails() .post(uri) .then().log().ifError() .extract(); } - public static ExtractableResponse get(final String uri, final String accessToken) { + public static ExtractableResponse post(final String uri, final String accessToken, final Object body) { return RestAssured .given().log().ifValidationFails() .auth().preemptive().oauth2(accessToken) + .contentType(APPLICATION_JSON_VALUE) + .body(body) .when().log().ifValidationFails() - .get(uri) + .post(uri) .then().log().ifError() .extract(); } - public static ExtractableResponse get(final String uri, - final String pathParamName, - final Long id, - final String accessToken + public static ExtractableResponse post(final String uri, + final String accessToken, + final PathParams pathParams, + final Object body ) { return RestAssured .given().log().ifValidationFails() .auth().preemptive().oauth2(accessToken) - .pathParam(pathParamName, id) + .contentType(APPLICATION_JSON_VALUE) + .pathParams(pathParams.getValues()) + .body(body) .when().log().ifValidationFails() - .get(uri) + .post(uri) .then().log().ifError() .extract(); } - public static ExtractableResponse get(final String uri, final String pathParamName, final Long id) { + public static ExtractableResponse get(final String uri, final PathParams pathParams) { return RestAssured .given().log().ifValidationFails() - .pathParam(pathParamName, id) + .pathParams(pathParams.getValues()) .when().log().ifValidationFails() .get(uri) .then().log().ifError() .extract(); } - public static ExtractableResponse get(final String uri, final Map queryParams) { + public static ExtractableResponse get(final String uri, final String accessToken) { return RestAssured .given().log().ifValidationFails() - .contentType(APPLICATION_JSON_VALUE) - .queryParams(queryParams) + .auth().preemptive().oauth2(accessToken) .when().log().ifValidationFails() .get(uri) .then().log().ifError() .extract(); } - public static ExtractableResponse get(final String uri, final String accessToken, final Map queryParams) { + public static ExtractableResponse get(final String uri, + final String accessToken, + final PathParams pathParams + ) { return RestAssured .given().log().ifValidationFails() .auth().preemptive().oauth2(accessToken) - .contentType(APPLICATION_JSON_VALUE) - .queryParams(queryParams) + .pathParams(pathParams.getValues()) .when().log().ifValidationFails() .get(uri) .then().log().ifError() .extract(); } - public static ExtractableResponse get(final String uri, final Map queryParams, final String accessToken) { + public static ExtractableResponse get(final String uri, final QueryParams queryParams) { return RestAssured .given().log().ifValidationFails() - .auth().preemptive().oauth2(accessToken) - .contentType(APPLICATION_JSON_VALUE) - .queryParams(queryParams) + .queryParams(queryParams.getValues()) .when().log().ifValidationFails() .get(uri) .then().log().ifError() .extract(); } - public static ExtractableResponse patch(final String uri, - final String pathParamName, - final Long id, - final Object requestBody, - final String accessToken - ) { + public static ExtractableResponse get(final String uri, final PathParams pathParams, final QueryParams queryParams) { return RestAssured .given().log().ifValidationFails() - .auth().preemptive().oauth2(accessToken) - .contentType(APPLICATION_JSON_VALUE) - .pathParam(pathParamName, id) - .body(requestBody) + .pathParams(pathParams.getValues()) + .queryParams(queryParams.getValues()) .when().log().ifValidationFails() - .patch(uri) + .get(uri) .then().log().ifError() .extract(); } + public static ExtractableResponse get(final String uri, final String accessToken, final QueryParams queryParams) { + return RestAssured + .given().log().ifValidationFails() + .auth().preemptive().oauth2(accessToken) + .queryParams(queryParams.getValues()) + .when().log().ifValidationFails() + .get(uri) + .then().log().ifError() + .extract(); + } - public static ExtractableResponse patch(final String uri, final String accessToken, final Object requestBody) { + public static ExtractableResponse patch(final String uri, final String accessToken, final PathParams pathParams) { return RestAssured .given().log().ifValidationFails() .auth().preemptive().oauth2(accessToken) - .contentType(APPLICATION_JSON_VALUE) - .body(requestBody) + .pathParams(pathParams.getValues()) .when().log().ifValidationFails() .patch(uri) .then().log().ifError() .extract(); } - public static ExtractableResponse patch(final String uri, final String pathParamName, final Long id, final String accessToken) { + public static ExtractableResponse patch(final String uri, final String accessToken, final Object body) { return RestAssured .given().log().ifValidationFails() .auth().preemptive().oauth2(accessToken) - .pathParam(pathParamName, id) + .contentType(APPLICATION_JSON_VALUE) + .body(body) .when().log().ifValidationFails() .patch(uri) .then().log().ifError() .extract(); } - public static ExtractableResponse delete(final String uri, final String pathParamName, final Long id) { + public static ExtractableResponse patch(final String uri, + final String accessToken, + final PathParams pathParams, + final Object body + ) { return RestAssured .given().log().ifValidationFails() - .pathParam(pathParamName, id) + .auth().preemptive().oauth2(accessToken) .contentType(APPLICATION_JSON_VALUE) + .pathParams(pathParams.getValues()) + .body(body) .when().log().ifValidationFails() - .delete(uri) + .patch(uri) .then().log().ifError() .extract(); } - public static ExtractableResponse delete(final String uri, final String accessToken, final String pathParamName, final Long id) { + public static ExtractableResponse delete(final String uri, final String accessToken, final PathParams pathParams) { return RestAssured .given().log().ifValidationFails() .auth().preemptive().oauth2(accessToken) - .pathParam(pathParamName, id) + .pathParams(pathParams.getValues()) .when().log().ifValidationFails() .delete(uri) .then().log().ifError() diff --git a/backend/baton/src/test/java/touch/baton/assure/common/JwtTestManager.java b/backend/baton/src/test/java/touch/baton/assure/common/JwtTestManager.java new file mode 100644 index 000000000..906c71dfa --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/common/JwtTestManager.java @@ -0,0 +1,27 @@ +package touch.baton.assure.common; + +import io.jsonwebtoken.Claims; +import org.springframework.boot.test.context.TestComponent; +import org.springframework.context.annotation.Profile; +import touch.baton.domain.member.vo.SocialId; +import touch.baton.infra.auth.jwt.JwtDecoder; + +import static touch.baton.fixture.vo.AuthorizationHeaderFixture.bearerAuthorizationHeader; + +@Profile("test") +@TestComponent +public class JwtTestManager { + + private final JwtDecoder jwtDecoder; + + public JwtTestManager(final JwtDecoder jwtDecoder) { + this.jwtDecoder = jwtDecoder; + } + + public SocialId parseToSocialId(final String accessToken) { + final Claims claims = jwtDecoder.parseAuthorizationHeader(bearerAuthorizationHeader(accessToken)); + final String socialId = claims.get("socialId", String.class); + + return new SocialId(socialId); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/common/OauthLoginTestManager.java b/backend/baton/src/test/java/touch/baton/assure/common/OauthLoginTestManager.java new file mode 100644 index 000000000..2c6309136 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/common/OauthLoginTestManager.java @@ -0,0 +1,26 @@ +package touch.baton.assure.common; + +import touch.baton.assure.oauth.OauthAssuredSupport; +import touch.baton.domain.oauth.OauthType; + +public class OauthLoginTestManager { + + public String 소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(final String 테스트용_사용자_MockAuthCode) { + OauthAssuredSupport + .클라이언트_요청() + .소셜_로그인을_위한_리다이렉트_URL을_요청한다(OauthType.GITHUB) + + .서버_응답() + .소셜_로그인을_위한_리다이렉트_URL_요청_성공을_검증한다(); + + final String 사용자_액세스_토큰 = OauthAssuredSupport + .클라이언트_요청() + .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, 테스트용_사용자_MockAuthCode) + + .서버_응답() + .AuthCode를_통해_소셜_토큰_발급_및_사용자_회원가입에_성공한다() + .액세스_토큰을_반환한다(); + + return 사용자_액세스_토큰; + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/common/PathParams.java b/backend/baton/src/test/java/touch/baton/assure/common/PathParams.java new file mode 100644 index 000000000..dec23dba9 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/common/PathParams.java @@ -0,0 +1,16 @@ +package touch.baton.assure.common; + +import java.util.Map; + +public class PathParams { + + private final Map values; + + public PathParams(final Map values) { + this.values = values; + } + + public Map getValues() { + return values; + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/common/QueryParams.java b/backend/baton/src/test/java/touch/baton/assure/common/QueryParams.java new file mode 100644 index 000000000..759dcc314 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/common/QueryParams.java @@ -0,0 +1,16 @@ +package touch.baton.assure.common; + +import java.util.Map; + +public class QueryParams { + + private final Map values; + + public QueryParams(final Map values) { + this.values = values; + } + + public Map getValues() { + return values; + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/feedback/SupporterFeedbackAssuredSupport.java b/backend/baton/src/test/java/touch/baton/assure/feedback/SupporterFeedbackAssuredSupport.java index e2dd8eec1..c1e029fc6 100644 --- a/backend/baton/src/test/java/touch/baton/assure/feedback/SupporterFeedbackAssuredSupport.java +++ b/backend/baton/src/test/java/touch/baton/assure/feedback/SupporterFeedbackAssuredSupport.java @@ -7,6 +7,8 @@ import touch.baton.assure.common.HttpStatusAndLocationHeader; import touch.baton.domain.feedback.service.SupporterFeedBackCreateRequest; +import java.util.List; + import static org.assertj.core.api.SoftAssertions.assertSoftly; @SuppressWarnings("NonAsciiCharacters") @@ -19,19 +21,27 @@ private SupporterFeedbackAssuredSupport() { return new SupporterFeedbackClientRequestBuilder(); } + public static SupporterFeedBackCreateRequest 서포터_피드백_요청(final String 리뷰_타입, + final List 디스크립션, + final Long 서포터_식별자값, + final Long 러너_게시글_식별자값 + ) { + return new SupporterFeedBackCreateRequest(리뷰_타입, 디스크립션, 서포터_식별자값, 러너_게시글_식별자값); + } + public static class SupporterFeedbackClientRequestBuilder { private ExtractableResponse response; private String accessToken; - public SupporterFeedbackClientRequestBuilder 토큰으로_로그인한다(final String 토큰) { - this.accessToken = 토큰; + public SupporterFeedbackClientRequestBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; return this; } public SupporterFeedbackClientRequestBuilder 서포터_피드백을_등록한다(final SupporterFeedBackCreateRequest 서포터_피드백_정보) { - response = AssuredSupport.post("/api/v1/feedback/supporter", 서포터_피드백_정보, accessToken); + response = AssuredSupport.post("/api/v1/feedback/supporter", accessToken, 서포터_피드백_정보); return this; } diff --git a/backend/baton/src/test/java/touch/baton/assure/feedback/SupporterFeedbackCreateAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/feedback/SupporterFeedbackCreateAssuredTest.java index 5950ca7fa..e5aaf8775 100644 --- a/backend/baton/src/test/java/touch/baton/assure/feedback/SupporterFeedbackCreateAssuredTest.java +++ b/backend/baton/src/test/java/touch/baton/assure/feedback/SupporterFeedbackCreateAssuredTest.java @@ -1,55 +1,103 @@ package touch.baton.assure.feedback; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; import touch.baton.assure.common.HttpStatusAndLocationHeader; +import touch.baton.assure.runnerpost.RunnerPostAssuredCreateSupport; +import touch.baton.assure.runnerpost.RunnerPostAssuredSupport; import touch.baton.config.AssuredTestConfig; -import touch.baton.domain.feedback.service.SupporterFeedBackCreateRequest; -import touch.baton.domain.member.Member; -import touch.baton.domain.runner.Runner; -import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.config.infra.auth.oauth.authcode.MockAuthCodes; +import touch.baton.domain.member.vo.SocialId; 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 java.time.LocalDateTime; import java.util.List; import static org.springframework.http.HttpStatus.CREATED; -import static touch.baton.fixture.domain.SupporterFixture.create; -import static touch.baton.fixture.vo.DeadlineFixture.deadline; +import static touch.baton.assure.feedback.SupporterFeedbackAssuredSupport.서포터_피드백_요청; +import static touch.baton.assure.runnerpost.RunnerPostAssuredCreateSupport.러너_게시글_생성_요청; +import static touch.baton.assure.runnerpost.RunnerPostAssuredSupport.러너의_서포터_선택_요청; @SuppressWarnings("NonAsciiCharacters") class SupporterFeedbackCreateAssuredTest extends AssuredTestConfig { - private static String 토큰; - private Runner 피드백할_러너; - - @BeforeEach - void setUp() { - final String 소셜_아이디 = "hongSile"; - final Member 사용자 = memberRepository.save(MemberFixture.createWithSocialId(소셜_아이디)); - 피드백할_러너 = runnerRepository.save(RunnerFixture.createRunner(사용자)); - 토큰 = login(소셜_아이디); - } - @Test void 러너가_서포터_피드백을_등록한다() { // given - final Member 사용자_에단 = memberRepository.save(MemberFixture.createEthan()); - final Supporter 리뷰해준_서포터 = supporterRepository.save(create(사용자_에단)); - final RunnerPost 리뷰_완료한_게시글 = runnerPostRepository.save(RunnerPostFixture.create(피드백할_러너, 리뷰해준_서포터, deadline(LocalDateTime.now().plusHours(100)))); + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Supporter 서포터_헤나 = supporterRepository.getBySocialId(헤나_소셜_아이디); + + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); - final SupporterFeedBackCreateRequest 서포터_피드백_요청 = new SupporterFeedBackCreateRequest("GOOD", List.of("코드리뷰가 맛있어요.", "말투가 친절해요."), 리뷰해준_서포터.getId(), 리뷰_완료한_게시글.getId()); + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + 러너가_서포터의_리뷰_신청_선택에_성공한다(서포터_헤나, 디투_액세스_토큰, 디투_러너_게시글_식별자값); + 서포터가_러너_게시글의_리뷰를_완료로_변경하는_것을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); - // when, then + // then SupporterFeedbackAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(토큰) - .서포터_피드백을_등록한다(서포터_피드백_요청) + .액세스_토큰으로_로그인한다(디투_액세스_토큰) + .서포터_피드백을_등록한다( + 서포터_피드백_요청("GOOD", List.of("코드리뷰가 맛있어요.", "말투가 친절해요."), 서포터_헤나.getId(), 디투_러너_게시글_식별자값) + ) .서버_응답() .서포터_피드백_등록_성공을_검증한다(new HttpStatusAndLocationHeader(CREATED, "/api/v1/feedback/supporter")); } + + private Long 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(final String 헤나_액세스_토큰) { + return RunnerPostAssuredCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너가_러너_게시글을_작성한다( + 러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + List.of("자바", "스프링"), + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + ) + ) + + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); + } + + private void 서포터가_러너_게시글에_리뷰_신청을_성공한다(final String 헤나_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + RunnerPostAssuredCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .서포터가_러너_게시글에_리뷰를_신청한다(디투_러너_게시글_식별자값, "안녕하세요. 서포터 헤나입니다.") + + .서버_응답() + .서포터가_러너_게시글에_리뷰_신청_성공을_검증한다(디투_러너_게시글_식별자값); + } + + private void 러너가_서포터의_리뷰_신청_선택에_성공한다(final Supporter 서포터_헤나, final String 디투_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + RunnerPostAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(디투_액세스_토큰) + .러너가_서포터를_선택한다(디투_러너_게시글_식별자값, 러너의_서포터_선택_요청(서포터_헤나.getId())) + + .서버_응답() + .러너_게시글에_서포터가_성공적으로_선택되었는지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); + } + + private void 서포터가_러너_게시글의_리뷰를_완료로_변경하는_것을_성공한다(final String 헤나_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + RunnerPostAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .서포터가_리뷰를_완료하고_리뷰완료_버튼을_누른다(디투_러너_게시글_식별자값) + + .서버_응답() + .러너_게시글이_성공적으로_리뷰_완료_상태인지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); + } + + // FIXME: 2023/09/19 피드백 실패 테스트 추가해줘잉 } diff --git a/backend/baton/src/test/java/touch/baton/assure/member/MemberAssuredSupport.java b/backend/baton/src/test/java/touch/baton/assure/member/MemberAssuredSupport.java index 3dd5d0062..48fe463eb 100644 --- a/backend/baton/src/test/java/touch/baton/assure/member/MemberAssuredSupport.java +++ b/backend/baton/src/test/java/touch/baton/assure/member/MemberAssuredSupport.java @@ -28,12 +28,12 @@ public static class MemberClientRequestBuilder { private String accessToken; - public MemberClientRequestBuilder 로그인_한다(final String 토큰) { - accessToken = 토큰; + public MemberClientRequestBuilder 액세스_토큰으로_로그인_한다(final String 액세스_토큰) { + accessToken = 액세스_토큰; return this; } - public MemberClientRequestBuilder 사용자_본인_프로필을_가지고_있는_토큰으로_조회한다() { + public MemberClientRequestBuilder 사용자_본인_프로필을_가지고_있는_액세스_토큰으로_조회한다() { response = AssuredSupport.get("api/v1/profile/me", accessToken); return this; } diff --git a/backend/baton/src/test/java/touch/baton/assure/member/MemberBranchAssuredSupport.java b/backend/baton/src/test/java/touch/baton/assure/member/MemberBranchAssuredSupport.java new file mode 100644 index 000000000..1cad5b149 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/member/MemberBranchAssuredSupport.java @@ -0,0 +1,64 @@ +package touch.baton.assure.member; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import touch.baton.assure.common.AssuredSupport; +import touch.baton.assure.common.HttpStatusAndLocationHeader; +import touch.baton.domain.member.service.dto.GithubRepoNameRequest; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.springframework.http.HttpHeaders.LOCATION; + +@SuppressWarnings("NonAsciiCharacters") +class MemberBranchAssuredSupport { + + private MemberBranchAssuredSupport() { + } + + public static MemberClientRequestBuilder 클라이언트_요청() { + return new MemberClientRequestBuilder(); + } + + public static class MemberClientRequestBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public MemberClientRequestBuilder 액세스_토큰으로_로그인_한다(final String 액세스_토큰) { + accessToken = 액세스_토큰; + return this; + } + + public MemberClientRequestBuilder 사용자_본인_프로필을_가지고_있는_액세스_토큰으로_조회한다() { + response = AssuredSupport.get("api/v1/profile/me", accessToken); + return this; + } + + public MemberClientRequestBuilder 입력받은_레포에_사용자_github_계정명으로_된_브랜치를_생성한다(final GithubRepoNameRequest 레포_이름_요청) { + response = AssuredSupport.post("/api/v1/branch", accessToken, 레포_이름_요청); + return this; + } + + public MemberServerResponseBuilder 서버_응답() { + return new MemberServerResponseBuilder(response); + } + } + + public static class MemberServerResponseBuilder { + + private final ExtractableResponse response; + + public MemberServerResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + + public void 레포에_브랜치_등록_성공을_검증한다(final HttpStatusAndLocationHeader 예상_성공_응답) { + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(예상_성공_응답.getHttpStatus().value()); + softly.assertThat(response.header(LOCATION)).contains(예상_성공_응답.getLocation()); + } + ); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/member/MemberBranchCreateAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/member/MemberBranchCreateAssuredTest.java new file mode 100644 index 000000000..0f1d19700 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/member/MemberBranchCreateAssuredTest.java @@ -0,0 +1,27 @@ +package touch.baton.assure.member; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.common.HttpStatusAndLocationHeader; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.MockAuthCodes; +import touch.baton.domain.member.service.dto.GithubRepoNameRequest; + +import static org.springframework.http.HttpStatus.CREATED; + +@SuppressWarnings("NonAsciiCharacters") +class MemberBranchCreateAssuredTest extends AssuredTestConfig { + + @Test + void 로그인_한_사용자가_요청한_레포의_브랜치를_생성한다() { + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.ditooAuthCode()); + final GithubRepoNameRequest 브랜치_생성_요청 = new GithubRepoNameRequest("ditoo"); + + MemberBranchAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인_한다(디투_액세스_토큰) + .입력받은_레포에_사용자_github_계정명으로_된_브랜치를_생성한다(브랜치_생성_요청) + + .서버_응답() + .레포에_브랜치_등록_성공을_검증한다(new HttpStatusAndLocationHeader(CREATED, "/api/v1/profile/me")); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/member/MemberReadWithLoginedMemberAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/member/MemberReadWithLoginedMemberAssuredTest.java index 21be39d16..b6b06dc2c 100644 --- a/backend/baton/src/test/java/touch/baton/assure/member/MemberReadWithLoginedMemberAssuredTest.java +++ b/backend/baton/src/test/java/touch/baton/assure/member/MemberReadWithLoginedMemberAssuredTest.java @@ -2,26 +2,28 @@ import org.junit.jupiter.api.Test; import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.MockAuthCodes; import touch.baton.domain.member.Member; -import touch.baton.fixture.domain.MemberFixture; +import touch.baton.domain.member.vo.SocialId; import static touch.baton.assure.member.MemberAssuredSupport.로그인한_사용자_프로필_응답; @SuppressWarnings("NonAsciiCharacters") -public class MemberReadWithLoginedMemberAssuredTest extends AssuredTestConfig { +class MemberReadWithLoginedMemberAssuredTest extends AssuredTestConfig { @Test - void 로그인_한_맴버_프로필을_조회한다() { - final String 디투_소셜_id = "ditooSocialId"; - final Member 사용자_디투 = memberRepository.save(MemberFixture.createWithSocialId(디투_소셜_id)); - final String 디투_액세스_토큰 = login(디투_소셜_id); + void 로그인_한_사용자_프로필을_조회한다() { + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Member 사용자_헤나 = memberRepository.getBySocialId(헤나_소셜_아이디); MemberAssuredSupport .클라이언트_요청() - .로그인_한다(디투_액세스_토큰) - .사용자_본인_프로필을_가지고_있는_토큰으로_조회한다() + .액세스_토큰으로_로그인_한다(헤나_액세스_토큰) + .사용자_본인_프로필을_가지고_있는_액세스_토큰으로_조회한다() .서버_응답() - .로그인한_사용자_프로필_조회_성공을_검증한다(로그인한_사용자_프로필_응답(사용자_디투)); + .로그인한_사용자_프로필_조회_성공을_검증한다(로그인한_사용자_프로필_응답(사용자_헤나)); } } diff --git a/backend/baton/src/test/java/touch/baton/assure/oauth/OauthAssuredSupport.java b/backend/baton/src/test/java/touch/baton/assure/oauth/OauthAssuredSupport.java new file mode 100644 index 000000000..a416eb065 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/oauth/OauthAssuredSupport.java @@ -0,0 +1,157 @@ +package touch.baton.assure.oauth; + +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.springframework.http.HttpStatus; +import touch.baton.assure.common.AssuredSupport; +import touch.baton.assure.common.PathParams; +import touch.baton.assure.common.QueryParams; +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.member.Member; +import touch.baton.domain.oauth.OauthType; +import touch.baton.domain.oauth.token.AccessToken; +import touch.baton.domain.oauth.token.ExpireDate; +import touch.baton.domain.oauth.token.RefreshToken; +import touch.baton.domain.oauth.token.Token; +import touch.baton.domain.oauth.token.Tokens; + +import java.time.LocalDateTime; +import java.util.Map; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.HttpHeaders.SET_COOKIE; + +public class OauthAssuredSupport { + + private OauthAssuredSupport() { + } + + public static OauthClientRequestBuilder 클라이언트_요청() { + return new OauthClientRequestBuilder(); + } + + @SuppressWarnings("NonAsciiCharacters") + public static class OauthClientRequestBuilder { + + private ExtractableResponse response; + private String accessToken; + + public OauthClientRequestBuilder 액세스_토큰으로_로그인_한다(final String 액세스_토큰) { + accessToken = 액세스_토큰; + + return this; + } + + public OauthClientRequestBuilder 소셜_로그인을_위한_리다이렉트_URL을_요청한다(final OauthType 소셜_타입) { + response = RestAssured + .given().log().ifValidationFails() + .pathParams(Map.of("oauthType", 소셜_타입)) + .redirects().follow(false) + .when().log().ifValidationFails() + .get("/api/v1/oauth/{oauthType}") + .then().log().ifError() + .extract(); + + return this; + } + + public OauthClientRequestBuilder AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(final OauthType 소셜_타입, final String 사용자의_AuthCode) { + response = AssuredSupport.get( + "/api/v1/oauth/login/{oauthType}", + new PathParams(Map.of("oauthType", 소셜_타입)), + new QueryParams(Map.of("code", 사용자의_AuthCode)) + ); + + return this; + } + + public OauthClientRequestBuilder 기간이_만료된_액세스_토큰으로_프로필_조회하려_한다() { + response = AssuredSupport.get("/api/v1/profile/runner/me", accessToken); + + return this; + } + + public OauthClientRequestBuilder 기간_만료_액세스_토큰과_리프레시_토큰으로_리프레시_요청한다(final String 기간_만료_액세스_토큰, final String 리프레시_토큰) { + response = AssuredSupport.post("/api/v1/oauth/refresh", 기간_만료_액세스_토큰, 리프레시_토큰); + + return this; + } + + public OauthClientRequestBuilder 리프레시_토큰_없이_액세스_토큰만으로_리프레시_요청한다() { + response = AssuredSupport.post("/api/v1/oauth/refresh", accessToken); + + return this; + } + + public OauthClientRequestBuilder 리프레시를_요청한다() { + response = AssuredSupport.post("/api/v1/oauth/refresh"); + + return this; + } + + public OauthServerResponseBuilder 서버_응답() { + return new OauthServerResponseBuilder(response); + } + } + + public static class OauthServerResponseBuilder { + + private final ExtractableResponse response; + + public OauthServerResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + + public OauthServerResponseBuilder 소셜_로그인을_위한_리다이렉트_URL_요청_성공을_검증한다() { + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(HttpStatus.FOUND.value()); + }); + + return this; + } + + public OauthServerResponseBuilder AuthCode를_통해_소셜_토큰_발급_및_사용자_회원가입에_성공한다() { + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + softly.assertThat(response.header(AUTHORIZATION)).isNotBlank(); + softly.assertThat(response.header(SET_COOKIE)).isNotBlank(); + }); + + return this; + } + + public String 액세스_토큰을_반환한다() { + return response.header(AUTHORIZATION); + } + + public Tokens 액세스_토큰과_리프레시_토큰을_반환한다(final Member ethan) { + final String accessToken = response.header(AUTHORIZATION); + + final LocalDateTime expireDate = LocalDateTime.now().plusDays(30); + final RefreshToken refreshToken = RefreshToken.builder() + .member(ethan) + .token(new Token(response.cookie("refreshToken"))) + .expireDate(new ExpireDate(expireDate)) + .build(); + + return new Tokens(new AccessToken(accessToken), refreshToken); + } + + public void 새로운_액세스_토큰과_리프레시_토큰을_반환한다() { + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + softly.assertThat(response.header(AUTHORIZATION)).isNotBlank(); + softly.assertThat(response.header(SET_COOKIE)).isNotBlank(); + }); + } + + public void 오류가_발생한다(final ClientErrorCode clientErrorCode) { + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(clientErrorCode.getHttpStatus().value()); + softly.assertThat(response.jsonPath().getString("errorCode")).isEqualTo(clientErrorCode.getErrorCode()); + }); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/oauth/OauthCreateAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/oauth/OauthCreateAssuredTest.java new file mode 100644 index 000000000..557c24292 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/oauth/OauthCreateAssuredTest.java @@ -0,0 +1,27 @@ +package touch.baton.assure.oauth; + +import org.junit.jupiter.api.Test; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.MockAuthCodes; +import touch.baton.domain.oauth.OauthType; + +@SuppressWarnings("NonAsciiCharacters") +class OauthCreateAssuredTest extends AssuredTestConfig { + + @Test + void 사용자는_소셜_회원가입을_성공한다() { + OauthAssuredSupport + .클라이언트_요청() + .소셜_로그인을_위한_리다이렉트_URL을_요청한다(OauthType.GITHUB) + + .서버_응답() + .소셜_로그인을_위한_리다이렉트_URL_요청_성공을_검증한다(); + + OauthAssuredSupport + .클라이언트_요청() + .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, MockAuthCodes.hyenaAuthCode()) + + .서버_응답() + .AuthCode를_통해_소셜_토큰_발급_및_사용자_회원가입에_성공한다(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/oauth/OauthRefreshTokenAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/oauth/OauthRefreshTokenAssuredTest.java new file mode 100644 index 000000000..3b0d97b2a --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/oauth/OauthRefreshTokenAssuredTest.java @@ -0,0 +1,216 @@ +package touch.baton.assure.oauth; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.MockAuthCodes; +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.member.vo.SocialId; +import touch.baton.domain.oauth.OauthType; +import touch.baton.domain.oauth.token.ExpireDate; +import touch.baton.domain.oauth.token.Token; +import touch.baton.domain.oauth.token.Tokens; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.vo.ExpireDateFixture; +import touch.baton.infra.auth.jwt.JwtEncoder; + +import java.time.LocalDateTime; +import java.util.Map; + +@SuppressWarnings("NonAsciiCharacters") +class OauthRefreshTokenAssuredTest extends AssuredTestConfig { + + @Autowired + private JwtEncoder jwtExpireEncoder; + + @Test + void 액세스_토큰이_없으면_예외를_반환한다() { + OauthAssuredSupport + .클라이언트_요청() + .소셜_로그인을_위한_리다이렉트_URL을_요청한다(OauthType.GITHUB) + + .서버_응답() + .소셜_로그인을_위한_리다이렉트_URL_요청_성공을_검증한다(); + + final Tokens 액세스_토큰과_리프레시_토큰 = OauthAssuredSupport + .클라이언트_요청() + .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, MockAuthCodes.ethanAuthCode()) + + .서버_응답() + .AuthCode를_통해_소셜_토큰_발급_및_사용자_회원가입에_성공한다() + .액세스_토큰과_리프레시_토큰을_반환한다(MemberFixture.createEthan()); + + OauthAssuredSupport + .클라이언트_요청() + .리프레시를_요청한다() + + .서버_응답() + .오류가_발생한다(ClientErrorCode.OAUTH_AUTHORIZATION_VALUE_IS_NULL); + } + + @Test + void 리프래시_토큰이_없으면_예외를_반환한다() { + OauthAssuredSupport + .클라이언트_요청() + .소셜_로그인을_위한_리다이렉트_URL을_요청한다(OauthType.GITHUB) + + .서버_응답() + .소셜_로그인을_위한_리다이렉트_URL_요청_성공을_검증한다(); + + final Tokens 액세스_토큰과_리프레시_토큰 = OauthAssuredSupport + .클라이언트_요청() + .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, MockAuthCodes.ethanAuthCode()) + + .서버_응답() + .AuthCode를_통해_소셜_토큰_발급_및_사용자_회원가입에_성공한다() + .액세스_토큰과_리프레시_토큰을_반환한다(MemberFixture.createEthan()); + + OauthAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인_한다(액세스_토큰과_리프레시_토큰.accessToken().getValue()) + .리프레시_토큰_없이_액세스_토큰만으로_리프레시_요청한다() + + .서버_응답() + .오류가_발생한다(ClientErrorCode.REFRESH_TOKEN_IS_NOT_NULL); + } + + @Test + void 만료된_JWT_로_본인_프로필_조회_요청을_보내면_오류가_발생한다() { + OauthAssuredSupport + .클라이언트_요청() + .소셜_로그인을_위한_리다이렉트_URL을_요청한다(OauthType.GITHUB) + + .서버_응답() + .소셜_로그인을_위한_리다이렉트_URL_요청_성공을_검증한다(); + + final Tokens 액세스_토큰과_리프레시_토큰 = OauthAssuredSupport + .클라이언트_요청() + .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, MockAuthCodes.ethanAuthCode()) + + .서버_응답() + .AuthCode를_통해_소셜_토큰_발급_및_사용자_회원가입에_성공한다() + .액세스_토큰과_리프레시_토큰을_반환한다(MemberFixture.createEthan()); + + final String 기간_만료_액세스_토큰 = 기간_만료_액세스_토큰을_생성한다(액세스_토큰과_리프레시_토큰); + + OauthAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인_한다(기간_만료_액세스_토큰) + .기간이_만료된_액세스_토큰으로_프로필_조회하려_한다() + + .서버_응답() + .오류가_발생한다(ClientErrorCode.JWT_CLAIM_IS_ALREADY_EXPIRED); + } + + @Test + void 만료된_JWT와_리프레시_토큰을_가지고_리프레시_요청을_보내면_성공한다() { + OauthAssuredSupport + .클라이언트_요청() + .소셜_로그인을_위한_리다이렉트_URL을_요청한다(OauthType.GITHUB) + + .서버_응답() + .소셜_로그인을_위한_리다이렉트_URL_요청_성공을_검증한다(); + + final Tokens 액세스_토큰과_리프레시_토큰 = OauthAssuredSupport + .클라이언트_요청() + .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, MockAuthCodes.ethanAuthCode()) + + .서버_응답() + .AuthCode를_통해_소셜_토큰_발급_및_사용자_회원가입에_성공한다() + .액세스_토큰과_리프레시_토큰을_반환한다(MemberFixture.createEthan()); + + final String 기간_만료_액세스_토큰 = 기간_만료_액세스_토큰을_생성한다(액세스_토큰과_리프레시_토큰); + final String 리프레시_토큰 = 리프레시_토큰을_가져온다(액세스_토큰과_리프레시_토큰); + + OauthAssuredSupport + .클라이언트_요청() + .기간_만료_액세스_토큰과_리프레시_토큰으로_리프레시_요청한다(기간_만료_액세스_토큰, 리프레시_토큰) + + .서버_응답() + .새로운_액세스_토큰과_리프레시_토큰을_반환한다(); + } + + @Test + void 다른_사람의_JWT와_리프레시_토큰을_가지고_리프레시_요청을_보내면_실패한다() { + OauthAssuredSupport + .클라이언트_요청() + .소셜_로그인을_위한_리다이렉트_URL을_요청한다(OauthType.GITHUB) + + .서버_응답() + .소셜_로그인을_위한_리다이렉트_URL_요청_성공을_검증한다(); + + final Tokens 헤나_액세스_토큰과_리프레시_토큰 = OauthAssuredSupport + .클라이언트_요청() + .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, MockAuthCodes.hyenaAuthCode()) + + .서버_응답() + .AuthCode를_통해_소셜_토큰_발급_및_사용자_회원가입에_성공한다() + .액세스_토큰과_리프레시_토큰을_반환한다(MemberFixture.createHyena()); + + final Tokens 에단_액세스_토큰과_리프레시_토큰 = OauthAssuredSupport + .클라이언트_요청() + .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, MockAuthCodes.ethanAuthCode()) + + .서버_응답() + .AuthCode를_통해_소셜_토큰_발급_및_사용자_회원가입에_성공한다() + .액세스_토큰과_리프레시_토큰을_반환한다(MemberFixture.createEthan()); + + final String 기간_만료_헤나_액세스_토큰 = 기간_만료_액세스_토큰을_생성한다(헤나_액세스_토큰과_리프레시_토큰); + final String 에단_리프레시_토큰 = 리프레시_토큰을_가져온다(에단_액세스_토큰과_리프레시_토큰); + + OauthAssuredSupport + .클라이언트_요청() + .기간_만료_액세스_토큰과_리프레시_토큰으로_리프레시_요청한다(기간_만료_헤나_액세스_토큰, 에단_리프레시_토큰) + + .서버_응답() + .오류가_발생한다(ClientErrorCode.ACCESS_TOKEN_AND_REFRESH_TOKEN_HAVE_DIFFERENT_OWNER); + } + + private String 리프레시_토큰을_가져온다(final Tokens 액세스_토큰과_리프레시_토큰) { + return 액세스_토큰과_리프레시_토큰.refreshToken().getToken().getValue(); + } + + @Test + void 만료된_리프레시_토큰을_가지고_리프레시_요청을_보내면_실패한다() { + OauthAssuredSupport + .클라이언트_요청() + .소셜_로그인을_위한_리다이렉트_URL을_요청한다(OauthType.GITHUB) + + .서버_응답() + .소셜_로그인을_위한_리다이렉트_URL_요청_성공을_검증한다(); + + final Tokens 액세스_토큰과_리프레시_토큰 = OauthAssuredSupport + .클라이언트_요청() + .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, MockAuthCodes.ethanAuthCode()) + + .서버_응답() + .AuthCode를_통해_소셜_토큰_발급_및_사용자_회원가입에_성공한다() + .액세스_토큰과_리프레시_토큰을_반환한다(MemberFixture.createEthan()); + + final String 만료된_액세스_토큰 = 기간_만료_액세스_토큰을_생성한다(액세스_토큰과_리프레시_토큰); + final String 만료된_리프레시_토큰 = 만료된_리프레시_토큰을_가져온다(액세스_토큰과_리프레시_토큰); + + OauthAssuredSupport + .클라이언트_요청() + .기간_만료_액세스_토큰과_리프레시_토큰으로_리프레시_요청한다(만료된_액세스_토큰, 만료된_리프레시_토큰) + + .서버_응답() + .오류가_발생한다(ClientErrorCode.REFRESH_TOKEN_IS_ALREADY_EXPIRED); + } + + private String 기간_만료_액세스_토큰을_생성한다(final Tokens 액세스_토큰과_리프레시_토큰) { + final SocialId 소셜_아이디 = 액세스_토큰과_리프레시_토큰.refreshToken().getMember().getSocialId(); + final String 기간_만료_액세스_토큰 = jwtExpireEncoder.jwtToken(Map.of("socialId", 소셜_아이디.getValue())); + + return 기간_만료_액세스_토큰; + } + + private String 만료된_리프레시_토큰을_가져온다(final Tokens 액세스_토큰과_리프레시_토큰) { + final Token 토큰 = 액세스_토큰과_리프레시_토큰.refreshToken().getToken(); + final ExpireDate 기간_만료일 = ExpireDateFixture.expireDate(LocalDateTime.now().minusDays(14)); + + refreshTokenRepository.changeExpireDateByToken(토큰, 기간_만료일); + + return 토큰.getValue(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/repository/TestMemberRepository.java b/backend/baton/src/test/java/touch/baton/assure/repository/TestMemberRepository.java new file mode 100644 index 000000000..8234d1963 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/repository/TestMemberRepository.java @@ -0,0 +1,17 @@ +package touch.baton.assure.repository; + +import touch.baton.domain.member.Member; +import touch.baton.domain.member.repository.MemberRepository; +import touch.baton.domain.member.vo.SocialId; + +import java.util.Optional; + +public interface TestMemberRepository extends MemberRepository { + + default Member getBySocialId(final SocialId socialId) { + return findBySocialId(socialId) + .orElseThrow(() -> new IllegalArgumentException("테스트에서 Runner 를 SocialId 로 조회할 수 없습니다.")); + }; + + Optional findBySocialId(final SocialId socialId); +} diff --git a/backend/baton/src/test/java/touch/baton/assure/repository/TestRefreshTokenRepository.java b/backend/baton/src/test/java/touch/baton/assure/repository/TestRefreshTokenRepository.java new file mode 100644 index 000000000..7f37c856c --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/repository/TestRefreshTokenRepository.java @@ -0,0 +1,22 @@ +package touch.baton.assure.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import touch.baton.domain.oauth.token.ExpireDate; +import touch.baton.domain.oauth.token.RefreshToken; +import touch.baton.domain.oauth.token.Token; + +public interface TestRefreshTokenRepository extends JpaRepository { + + @Modifying + @Transactional + @Query(""" + update RefreshToken rt + set rt.expireDate = :expireDate + where rt.token = :token + """) + void changeExpireDateByToken(@Param("token") final Token token, @Param("expireDate") final ExpireDate expireDate); +} diff --git a/backend/baton/src/test/java/touch/baton/assure/repository/TestRunnerPostReadRepository.java b/backend/baton/src/test/java/touch/baton/assure/repository/TestRunnerPostReadRepository.java new file mode 100644 index 000000000..3e1d5244d --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/repository/TestRunnerPostReadRepository.java @@ -0,0 +1,18 @@ +package touch.baton.assure.repository; + +import touch.baton.domain.runnerpost.repository.RunnerPostReadRepository; +import touch.baton.domain.runnerpost.repository.dto.ApplicantCountDto; + +import java.util.List; + +public interface TestRunnerPostReadRepository extends RunnerPostReadRepository { + + default Long countApplicantByRunnerPostId(final Long runnerPostId) { + final List foundApplicants = countApplicantsByRunnerPostIds(List.of(runnerPostId)); + if (foundApplicants.isEmpty()) { + throw new IllegalArgumentException("테스트에서 러너 게시글 식별자값으로 서포터 지원자 수 조회에 실패하였습니다."); + } + + return foundApplicants.get(0).applicantCount(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/repository/TestRunnerPostRepository.java b/backend/baton/src/test/java/touch/baton/assure/repository/TestRunnerPostRepository.java new file mode 100644 index 000000000..2be576501 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/repository/TestRunnerPostRepository.java @@ -0,0 +1,13 @@ +package touch.baton.assure.repository; + +import org.springframework.data.repository.query.Param; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.repository.RunnerPostRepository; + +public interface TestRunnerPostRepository extends RunnerPostRepository { + + default RunnerPost getByRunnerPostId(@Param("runnerPostId") final Long runnerPostId) { + return joinMemberByRunnerPostId(runnerPostId) + .orElseThrow(() -> new IllegalArgumentException("테스트에서 RunnerPost 를 러너 게시글 식별자값(id) 로 조회할 수 없습니다.")); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/repository/TestRunnerRepository.java b/backend/baton/src/test/java/touch/baton/assure/repository/TestRunnerRepository.java new file mode 100644 index 000000000..2090c9465 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/repository/TestRunnerRepository.java @@ -0,0 +1,25 @@ +package touch.baton.assure.repository; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import touch.baton.domain.member.vo.SocialId; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runner.repository.RunnerRepository; + +import java.util.Optional; + +public interface TestRunnerRepository extends RunnerRepository { + + default Runner getBySocialId(final SocialId socialId) { + return joinMemberBySocialId(socialId) + .orElseThrow(() -> new IllegalArgumentException("테스트에서 Runner 를 SocialId 로 조회할 수 없습니다.")); + } + + @Query(""" + select r, m + from Runner r + join fetch Member m on m.id = r.member.id + where m.socialId = :socialId + """) + Optional joinMemberBySocialId(@Param("socialId") final SocialId socialId); +} diff --git a/backend/baton/src/test/java/touch/baton/assure/repository/TestSupporterRepository.java b/backend/baton/src/test/java/touch/baton/assure/repository/TestSupporterRepository.java new file mode 100644 index 000000000..de944ecd9 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/repository/TestSupporterRepository.java @@ -0,0 +1,25 @@ +package touch.baton.assure.repository; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import touch.baton.domain.member.vo.SocialId; +import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.supporter.repository.SupporterRepository; + +import java.util.Optional; + +public interface TestSupporterRepository extends SupporterRepository { + + default Supporter getBySocialId(final SocialId socialId) { + return joinMemberBySocialId(socialId) + .orElseThrow(() -> new IllegalArgumentException("테스트에서 Supporter 를 SocialId 로 조회할 수 없습니다.")); + } + + @Query(""" + select s, m + from Supporter s + join fetch Member m on m.id = s.member.id + where m.socialId = :socialId + """) + Optional joinMemberBySocialId(@Param("socialId") final SocialId socialId); +} diff --git a/backend/baton/src/test/java/touch/baton/assure/repository/TestSupporterRunnerPostRepository.java b/backend/baton/src/test/java/touch/baton/assure/repository/TestSupporterRunnerPostRepository.java new file mode 100644 index 000000000..052b798c9 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/repository/TestSupporterRunnerPostRepository.java @@ -0,0 +1,10 @@ +package touch.baton.assure.repository; + +import touch.baton.domain.supporter.repository.SupporterRunnerPostRepository; + +public interface TestSupporterRunnerPostRepository extends SupporterRunnerPostRepository { + + default Long getApplicantCountByRunnerPostId(final Long runnerPostId) { + return countByRunnerPostId(runnerPostId).orElse(0L); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/repository/TestTagRepository.java b/backend/baton/src/test/java/touch/baton/assure/repository/TestTagRepository.java new file mode 100644 index 000000000..b7e614433 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/repository/TestTagRepository.java @@ -0,0 +1,6 @@ +package touch.baton.assure.repository; + +import touch.baton.domain.tag.repository.TagRepository; + +public interface TestTagRepository extends TagRepository { +} diff --git a/backend/baton/src/test/java/touch/baton/assure/repository/TestTechnicalTagRepository.java b/backend/baton/src/test/java/touch/baton/assure/repository/TestTechnicalTagRepository.java new file mode 100644 index 000000000..252242b71 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/repository/TestTechnicalTagRepository.java @@ -0,0 +1,6 @@ +package touch.baton.assure.repository; + +import touch.baton.domain.technicaltag.repository.TechnicalTagRepository; + +public interface TestTechnicalTagRepository extends TechnicalTagRepository { +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runner/RunnerAssuredSupport.java b/backend/baton/src/test/java/touch/baton/assure/runner/RunnerAssuredSupport.java index d2708e749..154b91427 100644 --- a/backend/baton/src/test/java/touch/baton/assure/runner/RunnerAssuredSupport.java +++ b/backend/baton/src/test/java/touch/baton/assure/runner/RunnerAssuredSupport.java @@ -2,8 +2,10 @@ import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; -import org.springframework.http.HttpStatus; +import org.springframework.http.HttpHeaders; import touch.baton.assure.common.AssuredSupport; +import touch.baton.assure.common.HttpStatusAndLocationHeader; +import touch.baton.assure.common.PathParams; import touch.baton.domain.common.exception.ClientErrorCode; import touch.baton.domain.common.response.ErrorResponse; import touch.baton.domain.runner.Runner; @@ -11,6 +13,9 @@ import touch.baton.domain.runner.controller.response.RunnerResponse; import touch.baton.domain.runner.service.dto.RunnerUpdateRequest; +import java.util.List; +import java.util.Map; + import static org.assertj.core.api.SoftAssertions.assertSoftly; @SuppressWarnings("NonAsciiCharacters") @@ -23,8 +28,51 @@ private RunnerAssuredSupport() { return new RunnerClientRequestBuilder(); } - public static RunnerResponse.MyProfile 러너_본인_프로필_응답(final Runner 러너) { - return RunnerResponse.MyProfile.from(러너); + public static RunnerResponse.MyProfile 러너_본인_프로필_응답(final Runner 러너, final List 러너_태그_목록) { + return new RunnerResponse.MyProfile( + 러너.getMember().getMemberName().getValue(), + 러너.getMember().getCompany().getValue(), + 러너.getMember().getImageUrl().getValue(), + 러너.getMember().getGithubUrl().getValue(), + 러너.getIntroduction().getValue(), + 러너_태그_목록 + ); + } + + public static RunnerUpdateRequest 러너_본인_프로필_수정_요청(final String 러너_이름, + final String 회사, + final String 러너_소개글, + final List 러너_기술태그_목록 + ) { + return new RunnerUpdateRequest(러너_이름, 회사, 러너_소개글, 러너_기술태그_목록); + } + + public static RunnerProfileResponse.Detail 러너_프로필_상세_응답(final Runner 러너) { + final List 태그_목록 = 러너.getRunnerTechnicalTags().getRunnerTechnicalTags().stream() + .map(runnerTechnicalTag -> runnerTechnicalTag.getTechnicalTag().getTagName().getValue()) + .toList(); + + return new RunnerProfileResponse.Detail( + 러너.getId(), + 러너.getMember().getMemberName().getValue(), + 러너.getMember().getImageUrl().getValue(), + 러너.getMember().getGithubUrl().getValue(), + 러너.getIntroduction().getValue(), + 러너.getMember().getCompany().getValue(), + 태그_목록 + ); + } + + public static RunnerProfileResponse.Detail 러너_프로필_상세_응답(final Runner 러너, final RunnerUpdateRequest 러너_본인_프로필_수정_요청) { + return new RunnerProfileResponse.Detail( + 러너.getId(), + 러너_본인_프로필_수정_요청.name(), + 러너.getMember().getImageUrl().getValue(), + 러너.getMember().getGithubUrl().getValue(), + 러너_본인_프로필_수정_요청.introduction(), + 러너_본인_프로필_수정_요청.company(), + 러너_본인_프로필_수정_요청.technicalTags() + ); } public static class RunnerClientRequestBuilder { @@ -33,29 +81,29 @@ public static class RunnerClientRequestBuilder { private String accessToken; - public RunnerClientRequestBuilder 토큰으로_로그인한다(final String 토큰) { - this.accessToken = 토큰; + public RunnerClientRequestBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; return this; } - public RunnerClientRequestBuilder 러너_본인_프로필을_가지고_있는_토큰으로_조회한다() { + public RunnerClientRequestBuilder 러너_본인_프로필을_가지고_있는_액세스_토큰으로_조회한다() { response = AssuredSupport.get("/api/v1/profile/runner/me", accessToken); return this; } public RunnerClientRequestBuilder 러너_프로필을_상세_조회한다(final Long 러너_식별자) { - response = AssuredSupport.get("/api/v1/profile/runner/{runnerId}", "runnerId", 러너_식별자); + response = AssuredSupport.get("/api/v1/profile/runner/{runnerId}", new PathParams(Map.of("runnerId", 러너_식별자))); return this; } - public RunnerServerResponseBuilder 서버_응답() { - return new RunnerServerResponseBuilder(response); - } - public RunnerClientRequestBuilder 러너_본인_프로필을_수정한다(final RunnerUpdateRequest 러너_업데이트_요청) { response = AssuredSupport.patch("/api/v1/profile/runner/me", accessToken, 러너_업데이트_요청); return this; } + + public RunnerServerResponseBuilder 서버_응답() { + return new RunnerServerResponseBuilder(response); + } } public static class RunnerServerResponseBuilder { @@ -85,6 +133,7 @@ public RunnerServerResponseBuilder(final ExtractableResponse response) public void 러너_프로필_상세_조회를_검증한다(final RunnerProfileResponse.Detail 러너_프로필_상세_응답) { final RunnerProfileResponse.Detail actual = this.response.as(RunnerProfileResponse.Detail.class); + assertSoftly(softly -> { softly.assertThat(actual.runnerId()).isNotNull(); softly.assertThat(actual.name()).isEqualTo(러너_프로필_상세_응답.name()); @@ -97,10 +146,10 @@ public RunnerServerResponseBuilder(final ExtractableResponse response) ); } - public void 러너_본인_프로필_수정_성공을_검증한다(final HttpStatus HTTP_STATUS, final Long 러너_아이디) { + public void 러너_본인_프로필_수정_성공을_검증한다(final HttpStatusAndLocationHeader 응답상태_및_로케이션) { assertSoftly(softly -> { - softly.assertThat(response.statusCode()).isEqualTo(HTTP_STATUS.value()); - softly.assertThat(response.header("Location")).isNotNull(); + softly.assertThat(response.statusCode()).isEqualTo(응답상태_및_로케이션.getHttpStatus().value()); + softly.assertThat(response.header(HttpHeaders.LOCATION)).contains(응답상태_및_로케이션.getLocation()); }); } diff --git a/backend/baton/src/test/java/touch/baton/assure/runner/RunnerReadByRunnerIdAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runner/RunnerReadByRunnerIdAssuredTest.java index e8c0e16ac..f5310a32a 100644 --- a/backend/baton/src/test/java/touch/baton/assure/runner/RunnerReadByRunnerIdAssuredTest.java +++ b/backend/baton/src/test/java/touch/baton/assure/runner/RunnerReadByRunnerIdAssuredTest.java @@ -1,42 +1,46 @@ package touch.baton.assure.runner; import org.junit.jupiter.api.Test; +import touch.baton.assure.common.HttpStatusAndLocationHeader; import touch.baton.config.AssuredTestConfig; -import touch.baton.domain.member.Member; +import touch.baton.config.infra.auth.oauth.authcode.MockAuthCodes; +import touch.baton.domain.member.vo.SocialId; import touch.baton.domain.runner.Runner; -import touch.baton.domain.runner.controller.response.RunnerProfileResponse; -import touch.baton.domain.technicaltag.TechnicalTag; -import touch.baton.fixture.domain.MemberFixture; -import touch.baton.fixture.domain.RunnerFixture; -import touch.baton.fixture.domain.TechnicalTagFixture; +import touch.baton.domain.runner.service.dto.RunnerUpdateRequest; import java.util.List; +import static org.springframework.http.HttpStatus.NO_CONTENT; +import static touch.baton.assure.runner.RunnerAssuredSupport.러너_본인_프로필_수정_요청; +import static touch.baton.assure.runner.RunnerAssuredSupport.러너_프로필_상세_응답; + @SuppressWarnings("NonAsciiCharacters") class RunnerReadByRunnerIdAssuredTest extends AssuredTestConfig { @Test void 러너_프로필_조회에_성공한다() { // given - final Member 사용자_에단 = memberRepository.save(MemberFixture.createEthan()); - final TechnicalTag 자바_태그 = technicalTagRepository.save(TechnicalTagFixture.createJava()); - final TechnicalTag 리액트_태그 = technicalTagRepository.save(TechnicalTagFixture.createReact()); - final Runner 러너_에단 = runnerRepository.save(RunnerFixture.createRunner(사용자_에단, List.of(자바_태그, 리액트_태그))); - final RunnerProfileResponse.Detail 러너_프로필_조회_응답 = new RunnerProfileResponse.Detail( - 1L, - "에단", - "https://", - "https://github.com/", - "안녕하세요.", - "우아한테크코스 5기 백엔드", - List.of("Java", "React")); + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Runner 러너_헤나 = runnerRepository.getBySocialId(헤나_소셜_아이디); + + final RunnerUpdateRequest 러너_본인_프로필_수정_요청 = 러너_본인_프로필_수정_요청("수정된_헤나", "수정된_회사", "수정된_러너_소개글", List.of("자바", "스프링")); + RunnerAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_본인_프로필을_수정한다(러너_본인_프로필_수정_요청) + + .서버_응답() + .러너_본인_프로필_수정_성공을_검증한다(new HttpStatusAndLocationHeader(NO_CONTENT, "/api/v1/profile/runner/me")); // when, then RunnerAssuredSupport .클라이언트_요청() - .러너_프로필을_상세_조회한다(러너_에단.getId()) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_프로필을_상세_조회한다(러너_헤나.getId()) .서버_응답() - .러너_프로필_상세_조회를_검증한다(러너_프로필_조회_응답); + .러너_프로필_상세_조회를_검증한다(러너_프로필_상세_응답(러너_헤나, 러너_본인_프로필_수정_요청)); } } diff --git a/backend/baton/src/test/java/touch/baton/assure/runner/RunnerReadWithLoginedRunnerAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runner/RunnerReadWithLoginedRunnerAssuredTest.java index 1735d21a4..74df5065e 100644 --- a/backend/baton/src/test/java/touch/baton/assure/runner/RunnerReadWithLoginedRunnerAssuredTest.java +++ b/backend/baton/src/test/java/touch/baton/assure/runner/RunnerReadWithLoginedRunnerAssuredTest.java @@ -2,29 +2,32 @@ import org.junit.jupiter.api.Test; import touch.baton.config.AssuredTestConfig; -import touch.baton.domain.member.Member; +import touch.baton.config.infra.auth.oauth.authcode.MockAuthCodes; +import touch.baton.domain.member.vo.SocialId; import touch.baton.domain.runner.Runner; -import touch.baton.fixture.domain.MemberFixture; -import touch.baton.fixture.domain.RunnerFixture; + +import java.util.Collections; import static touch.baton.assure.runner.RunnerAssuredSupport.러너_본인_프로필_응답; -import static touch.baton.fixture.vo.IntroductionFixture.introduction; @SuppressWarnings("NonAsciiCharacters") class RunnerReadWithLoginedRunnerAssuredTest extends AssuredTestConfig { @Test - void 러너_본인_프로필을_가지고_있는_토큰으로_조회에_성공한다() { - final Member 사용자_헤나 = memberRepository.save(MemberFixture.createHyena()); - final Runner 러너_헤나 = runnerRepository.save(RunnerFixture.createRunner(introduction("안녕하세요"), 사용자_헤나)); - final String 로그인용_토큰 = login(사용자_헤나.getSocialId().getValue()); + void 러너_본인_프로필을_가지고_있는_액세스_토큰으로_조회에_성공한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Runner 러너_헤나 = runnerRepository.getBySocialId(헤나_소셜_아이디); + // when, then RunnerAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(로그인용_토큰) - .러너_본인_프로필을_가지고_있는_토큰으로_조회한다() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_본인_프로필을_가지고_있는_액세스_토큰으로_조회한다() .서버_응답() - .러너_본인_프로필_조회_성공을_검증한다(러너_본인_프로필_응답(러너_헤나)); + .러너_본인_프로필_조회_성공을_검증한다(러너_본인_프로필_응답(러너_헤나, Collections.emptyList())); } } diff --git a/backend/baton/src/test/java/touch/baton/assure/runner/RunnerUpdateAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runner/RunnerUpdateAssuredTest.java index ab2922b41..de7cc7755 100644 --- a/backend/baton/src/test/java/touch/baton/assure/runner/RunnerUpdateAssuredTest.java +++ b/backend/baton/src/test/java/touch/baton/assure/runner/RunnerUpdateAssuredTest.java @@ -1,58 +1,52 @@ package touch.baton.assure.runner; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +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.config.infra.auth.oauth.authcode.MockAuthCodes; import touch.baton.domain.runner.service.dto.RunnerUpdateRequest; -import touch.baton.fixture.domain.MemberFixture; -import touch.baton.fixture.domain.RunnerFixture; import java.util.List; import static org.springframework.http.HttpStatus.NO_CONTENT; +import static touch.baton.assure.runner.RunnerAssuredSupport.러너_본인_프로필_수정_요청; import static touch.baton.domain.common.exception.ClientErrorCode.COMPANY_IS_NULL; import static touch.baton.domain.common.exception.ClientErrorCode.NAME_IS_NULL; import static touch.baton.domain.common.exception.ClientErrorCode.RUNNER_INTRODUCTION_IS_NULL; import static touch.baton.domain.common.exception.ClientErrorCode.RUNNER_TECHNICAL_TAGS_ARE_NULL; @SuppressWarnings("NonAsciiCharacters") -public class RunnerUpdateAssuredTest extends AssuredTestConfig { - - private String 디투_액세스_토큰; - - private Runner 러너_디투; - - @BeforeEach - void setUp() { - final String 소셜_id = "judySocialId"; - final Member 사용자_주디 = memberRepository.save(MemberFixture.createWithSocialId(소셜_id)); - 러너_디투 = runnerRepository.save(RunnerFixture.createRunner(사용자_주디)); - 디투_액세스_토큰 = login(소셜_id); - } +class RunnerUpdateAssuredTest extends AssuredTestConfig { @Test void 러너_정보를_수정한다() { - final RunnerUpdateRequest 러너_업데이트_요청 = new RunnerUpdateRequest("업데이트된 이름", "업데이트된 소속", "업데이트된 자기소개", List.of("Java", "React")); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + + final RunnerUpdateRequest 러너_본인_프로필_수정_요청 = 러너_본인_프로필_수정_요청("수정된_헤나", "수정된_회사", "수정된_러너_소개글", List.of("자바", "스프링")); + // when, then RunnerAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(디투_액세스_토큰) - .러너_본인_프로필을_수정한다(러너_업데이트_요청) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_본인_프로필을_수정한다(러너_본인_프로필_수정_요청) .서버_응답() - .러너_본인_프로필_수정_성공을_검증한다(NO_CONTENT, 러너_디투.getId()); + .러너_본인_프로필_수정_성공을_검증한다(new HttpStatusAndLocationHeader(NO_CONTENT, "/api/v1/profile/runner/me")); } @Test void 러너_정보_수정_시에_이름이_없으면_예외가_발생한다() { - final RunnerUpdateRequest 러너_업데이트_요청 = new RunnerUpdateRequest(null, "업데이트된 소속", "업데이트된 자기소개", List.of("Java", "React")); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + + final RunnerUpdateRequest 러너_본인_프로필_수정_요청 = 러너_본인_프로필_수정_요청(null, "수정된_회사", "수정된_러너_소개글", List.of("자바", "스프링")); + // when, then RunnerAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(디투_액세스_토큰) - .러너_본인_프로필을_수정한다(러너_업데이트_요청) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_본인_프로필을_수정한다(러너_본인_프로필_수정_요청) .서버_응답() .러너_본인_프로필_수정_실패를_검증한다(NAME_IS_NULL); @@ -60,12 +54,15 @@ void setUp() { @Test void 서포터_정보_수정_시에_소속이_없으면_예외가_발생한다() { - final RunnerUpdateRequest 러너_업데이트_요청 = new RunnerUpdateRequest("업데이트된 이름", null, "업데이트된 자기소개", List.of("Java", "React")); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + // when, then + final RunnerUpdateRequest 러너_본인_프로필_수정_요청 = 러너_본인_프로필_수정_요청("수정된_헤나", null, "수정된_러너_소개글", List.of("자바", "스프링")); RunnerAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(디투_액세스_토큰) - .러너_본인_프로필을_수정한다(러너_업데이트_요청) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_본인_프로필을_수정한다(러너_본인_프로필_수정_요청) .서버_응답() .러너_본인_프로필_수정_실패를_검증한다(COMPANY_IS_NULL); @@ -73,12 +70,15 @@ void setUp() { @Test void 러너_정보_수정_시에_소개글이_없으면_예외가_발생한다() { - final RunnerUpdateRequest 러너_업데이트_요청 = new RunnerUpdateRequest("업데이트된 이름", "업데이트된 소속", null, List.of("Java", "React")); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + // when, then + final RunnerUpdateRequest 러너_본인_프로필_수정_요청 = 러너_본인_프로필_수정_요청("수정된_헤나", "수정된_회사", null, List.of("자바", "스프링")); RunnerAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(디투_액세스_토큰) - .러너_본인_프로필을_수정한다(러너_업데이트_요청) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_본인_프로필을_수정한다(러너_본인_프로필_수정_요청) .서버_응답() .러너_본인_프로필_수정_실패를_검증한다(RUNNER_INTRODUCTION_IS_NULL); @@ -86,12 +86,15 @@ void setUp() { @Test void 러너_정보_수정_시에_기술_태그가_없으면_예외가_발생한다() { - final RunnerUpdateRequest 러너_업데이트_요청 = new RunnerUpdateRequest("업데이트된 이름", "업데이트된 소속", "업데이트된 자기소개", null); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + // when, then + final RunnerUpdateRequest 러너_본인_프로필_수정_요청 = 러너_본인_프로필_수정_요청("수정된_헤나", "수정된_회사", "수정된_러너_소개글", null); RunnerAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(디투_액세스_토큰) - .러너_본인_프로필을_수정한다(러너_업데이트_요청) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_본인_프로필을_수정한다(러너_본인_프로필_수정_요청) .서버_응답() .러너_본인_프로필_수정_실패를_검증한다(RUNNER_TECHNICAL_TAGS_ARE_NULL); diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredCreateSupport.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredCreateSupport.java index 5d31cd378..573a75ab7 100644 --- a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredCreateSupport.java +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredCreateSupport.java @@ -4,6 +4,7 @@ import io.restassured.response.Response; import org.springframework.http.HttpStatus; import touch.baton.assure.common.AssuredSupport; +import touch.baton.assure.common.PathParams; import touch.baton.domain.runnerpost.service.dto.RunnerPostApplicantCreateRequest; import touch.baton.domain.runnerpost.service.dto.RunnerPostCreateRequest; @@ -28,9 +29,11 @@ private RunnerPostAssuredCreateSupport() { final List 태그_목록, final String 풀_리퀘스트, final LocalDateTime 마감기한, - final String 러너_게시글_내용 + final String 구현_내용, + final String 궁금한_내용, + final String 참고_사항 ) { - return new RunnerPostCreateRequest(러너_게시글_제목, 태그_목록, 풀_리퀘스트, 마감기한, 러너_게시글_내용); + return new RunnerPostCreateRequest(러너_게시글_제목, 태그_목록, 풀_리퀘스트, 마감기한, 구현_내용, 궁금한_내용, 참고_사항); } public static class RunnerPostClientRequestBuilder { @@ -39,8 +42,8 @@ public static class RunnerPostClientRequestBuilder { private String accessToken; - public RunnerPostClientRequestBuilder 토큰으로_로그인한다(final String accessToken) { - this.accessToken = accessToken; + public RunnerPostClientRequestBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; return this; } @@ -52,7 +55,7 @@ public static class RunnerPostClientRequestBuilder { public RunnerPostClientRequestBuilder 서포터가_러너_게시글에_리뷰를_신청한다(final Long 러너_게시글_식별자값, final String 리뷰_지원_메시지) { response = AssuredSupport.post("/api/v1/posts/runner/{runnerPostId}/application", accessToken, - Map.of("runnerPostId", 러너_게시글_식별자값), + new PathParams(Map.of("runnerPostId", 러너_게시글_식별자값)), new RunnerPostApplicantCreateRequest(리뷰_지원_메시지) ); return this; diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredCreateTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredCreateTest.java index 2102c179c..4339379dc 100644 --- a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredCreateTest.java +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredCreateTest.java @@ -2,16 +2,12 @@ import org.junit.jupiter.api.Test; import touch.baton.config.AssuredTestConfig; -import touch.baton.domain.member.Member; +import touch.baton.config.infra.auth.oauth.authcode.MockAuthCodes; +import touch.baton.domain.member.vo.SocialId; import touch.baton.domain.runner.Runner; import touch.baton.domain.runnerpost.controller.response.RunnerPostResponse; import touch.baton.domain.runnerpost.service.dto.RunnerPostCreateRequest; import touch.baton.domain.runnerpost.vo.ReviewStatus; -import touch.baton.domain.supporter.Supporter; -import touch.baton.domain.technicaltag.TechnicalTag; -import touch.baton.fixture.domain.MemberFixture; -import touch.baton.fixture.domain.RunnerFixture; -import touch.baton.fixture.domain.SupporterFixture; import java.util.List; @@ -19,36 +15,32 @@ import static touch.baton.assure.runnerpost.RunnerPostAssuredCreateSupport.러너_게시글_생성_요청; import static touch.baton.assure.runnerpost.RunnerPostAssuredSupport.러너_게시글_Detail_응답; import static touch.baton.domain.runnerpost.vo.ReviewStatus.NOT_STARTED; -import static touch.baton.fixture.domain.TechnicalTagFixture.createJava; -import static touch.baton.fixture.domain.TechnicalTagFixture.createSpring; -import static touch.baton.fixture.vo.IntroductionFixture.introduction; -import static touch.baton.fixture.vo.ReviewCountFixture.reviewCount; @SuppressWarnings("NonAsciiCharacters") class RunnerPostAssuredCreateTest extends AssuredTestConfig { @Test void 러너가_러너_게시글을_생성하고_서포터가_러너_게시글에_리뷰를_신청한다() { - final Runner 러너_에단 = 러너를_저장한다(MemberFixture.createEthan()); - final Supporter 서포터_헤나 = 서포터_헤나를_저장한다(); - - final String 에단_로그인_토큰 = login(러너_에단.getMember().getSocialId().getValue()); - final String 헤나_로그인_토큰 = login(서포터_헤나.getMember().getSocialId().getValue()); + final String 에단_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.ethanAuthCode()); + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); final RunnerPostCreateRequest 러너_게시글_생성_요청 = 러너_게시글_생성_요청을_생성한다(); final Long 에단의_러너_게시글_식별자값 = RunnerPostAssuredCreateSupport .클라이언트_요청() - .토큰으로_로그인한다(에단_로그인_토큰) + .액세스_토큰으로_로그인한다(에단_액세스_토큰) .러너가_러너_게시글을_작성한다(러너_게시글_생성_요청) .서버_응답() .러너_게시글_생성_성공을_검증한다() .생성한_러너_게시글의_식별자값을_반환한다(); + final SocialId 에단의_소셜_아이디 = jwtTestManager.parseToSocialId(에단_액세스_토큰); + final Runner 러너_에단 = runnerRepository.getBySocialId(에단의_소셜_아이디); + final RunnerPostResponse.Detail 리뷰가_시작되지_않은_에단의_러너_게시글_Detail_응답 = 러너_게시글_Detail_응답을_생성한다(러너_에단, 러너_게시글_생성_요청, NOT_STARTED, 에단의_러너_게시글_식별자값, 1, 0L, false); RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(에단_로그인_토큰) + .액세스_토큰으로_로그인한다(에단_액세스_토큰) .러너_게시글_식별자값으로_러너_게시글을_조회한다(에단의_러너_게시글_식별자값) .서버_응답() @@ -56,35 +48,22 @@ class RunnerPostAssuredCreateTest extends AssuredTestConfig { RunnerPostAssuredCreateSupport .클라이언트_요청() - .토큰으로_로그인한다(헤나_로그인_토큰) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) .서포터가_러너_게시글에_리뷰를_신청한다(에단의_러너_게시글_식별자값, "안녕하세요. 서포터 헤나입니다.") .서버_응답() .서포터가_러너_게시글에_리뷰_신청_성공을_검증한다(에단의_러너_게시글_식별자값); } - private Runner 러너를_저장한다(final Member 사용자) { - final Member 저장된_사용자 = memberRepository.save(사용자); - - return runnerRepository.save(RunnerFixture.createRunner(introduction("안녕하세요"), 저장된_사용자)); - } - - private Supporter 서포터_헤나를_저장한다() { - final TechnicalTag 자바_기술_태그 = technicalTagRepository.save(createJava()); - final TechnicalTag 스프링_기술_태그 = technicalTagRepository.save(createSpring()); - final List 기술_태그_목록 = List.of(자바_기술_태그, 스프링_기술_태그); - final Member 저장된_사용자_헤나 = memberRepository.save(MemberFixture.createHyena()); - - return supporterRepository.save(SupporterFixture.create(reviewCount(0), 저장된_사용자_헤나, 기술_태그_목록)); - } - private RunnerPostCreateRequest 러너_게시글_생성_요청을_생성한다() { return 러너_게시글_생성_요청( "러너 게시글 테스트 제목", List.of("java", "spring"), "https://github.com", now().plusHours(10), - "러너 게시글 내용"); + "러너 게시글 내용", + "게시글 궁금한 내용", + "참고 사항"); } private RunnerPostResponse.Detail 러너_게시글_Detail_응답을_생성한다(final Runner 러너, @@ -98,7 +77,9 @@ class RunnerPostAssuredCreateTest extends AssuredTestConfig { return 러너_게시글_Detail_응답( 러너_게시글_식별자값, 러너_게시글_생성_요청.title(), - 러너_게시글_생성_요청.contents(), + 러너_게시글_생성_요청.implementedContents(), + 러너_게시글_생성_요청.curiousContents(), + 러너_게시글_생성_요청.postscriptContents(), 러너_게시글_생성_요청.pullRequestUrl(), 러너_게시글_생성_요청.deadline(), 조회수, 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 5fd977dfc..1f3d0b374 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 @@ -9,15 +9,20 @@ import org.springframework.http.HttpStatus; import touch.baton.assure.common.AssuredSupport; import touch.baton.assure.common.HttpStatusAndLocationHeader; +import touch.baton.assure.common.PathParams; +import touch.baton.assure.common.QueryParams; import touch.baton.domain.common.response.ErrorResponse; import touch.baton.domain.common.response.PageResponse; import touch.baton.domain.runner.Runner; import touch.baton.domain.runner.controller.response.RunnerResponse; +import touch.baton.domain.runnerpost.RunnerPost; import touch.baton.domain.runnerpost.controller.response.RunnerPostResponse; +import touch.baton.domain.runnerpost.controller.response.SupporterRunnerPostResponse; import touch.baton.domain.runnerpost.controller.response.SupporterRunnerPostResponses; import touch.baton.domain.runnerpost.service.dto.RunnerPostCreateRequest; import touch.baton.domain.runnerpost.service.dto.RunnerPostUpdateRequest; import touch.baton.domain.runnerpost.vo.ReviewStatus; +import touch.baton.domain.supporter.Supporter; import java.time.LocalDateTime; import java.util.List; @@ -37,9 +42,15 @@ private RunnerPostAssuredSupport() { return new RunnerPostClientRequestBuilder(); } + public static RunnerPostUpdateRequest.SelectSupporter 러너의_서포터_선택_요청(final Long 러너가_선택한_서포터_식별자값) { + return new RunnerPostUpdateRequest.SelectSupporter(러너가_선택한_서포터_식별자값); + } + public static RunnerPostResponse.Detail 러너_게시글_Detail_응답(final Long 러너_게시글_식별자값, final String 제목, - final String 내용, + final String 구현_내용, + final String 궁금한_내용, + final String 참고_사항, final String 풀_리퀘스트, final LocalDateTime 마감기한, final int 조회수, @@ -53,7 +64,9 @@ private RunnerPostAssuredSupport() { return new RunnerPostResponse.Detail( 러너_게시글_식별자값, 제목, - 내용, + 구현_내용, + 궁금한_내용, + 참고_사항, 풀_리퀘스트, 마감기한, 조회수, @@ -66,8 +79,8 @@ private RunnerPostAssuredSupport() { ); } - public static PageResponse 서포터가_리뷰_완료한_러너_게시글_응답(final Pageable 페이징_정보, - final List 서포터가_연관된_러너_게시글_목록 + public static PageResponse 서포터와_연관된_러너_게시글_페이징_응답(final Pageable 페이징_정보, + final List 서포터가_연관된_러너_게시글_목록 ) { final Page 페이징된_서포터가_연관된_러너_게시글 = new PageImpl<>(서포터가_연관된_러너_게시글_목록, 페이징_정보, 서포터가_연관된_러너_게시글_목록.size()); final PageResponse 페이징된_서포터가_연관된_러너_게시글_응답 = PageResponse.from(페이징된_서포터가_연관된_러너_게시글); @@ -75,8 +88,43 @@ private RunnerPostAssuredSupport() { return 페이징된_서포터가_연관된_러너_게시글_응답; } - public static PageResponse 러너_게시글_전체_조회_응답(final Pageable 페이징_정보, - final List 러너_게시글_목록 + public static RunnerPostResponse.ReferencedBySupporter 서포터와_연관된_러너_게시글_응답(final RunnerPost 러너_게시글, + final List 러너_게시글_태그_목록, + final int 조회수, + final long 러너_게시글에_지원한_서포터수, + final ReviewStatus 리뷰_상태 + ) { + return new RunnerPostResponse.ReferencedBySupporter( + 러너_게시글.getId(), + 러너_게시글.getTitle().getValue(), + 러너_게시글.getDeadline().getValue(), + 러너_게시글_태그_목록, + 조회수, + 러너_게시글에_지원한_서포터수, + 리뷰_상태.name() + ); + } + + public static RunnerPostResponse.Simple 러너_게시글_Simple_응답(final RunnerPost 러너_게시글, + final int 조회수, + final long 지원한_서포터_수, + final ReviewStatus 리뷰_상태, + final List 러너_게시글_태그_목록 + ) { + return new RunnerPostResponse.Simple( + 러너_게시글.getId(), + 러너_게시글.getTitle().getValue(), + 러너_게시글.getDeadline().getValue(), + 조회수, + 지원한_서포터_수, + 리뷰_상태.name(), + RunnerResponse.Simple.from(러너_게시글.getRunner()), + 러너_게시글_태그_목록 + ); + } + + public static PageResponse 러너_게시글_전체_Simple_페이징_응답(final Pageable 페이징_정보, + final List 러너_게시글_목록 ) { final Page 페이징된_러너_게시글 = new PageImpl<>(러너_게시글_목록, 페이징_정보, 러너_게시글_목록.size()); final PageResponse 페이징된_러너_게시글_응답 = PageResponse.from(페이징된_러너_게시글); @@ -84,6 +132,26 @@ private RunnerPostAssuredSupport() { return 페이징된_러너_게시글_응답; } + public static RunnerPostResponse.SimpleInMyPage 마이페이지_러너_게시글_SimpleInMyPage_응답(final RunnerPost 러너_게시글, + final Long 지원한_서포터_식별자값, + final int 조회수, + final long 지원한_서포터_수, + final ReviewStatus 리뷰_상태, + final List 러너_게시글_태그_목록 + ) { + return new RunnerPostResponse.SimpleInMyPage( + 러너_게시글.getId(), + 지원한_서포터_식별자값, + 러너_게시글.getTitle().getValue(), + 러너_게시글.getDeadline().getValue(), + 러너_게시글_태그_목록, + 조회수, + 지원한_서포터_수, + 리뷰_상태.name(), + 러너_게시글.getIsReviewed().getValue() + ); + } + public static PageResponse 마이페이지_러너_게시글_응답(final Pageable 페이징_정보, final List 마이페이지_러너_게시글_목록 ) { @@ -93,29 +161,55 @@ private RunnerPostAssuredSupport() { return 페이징된_마이페이지_러너_게시글_응답; } + public static SupporterRunnerPostResponse.Detail 지원한_서포터_응답(final Supporter 지원한_서포터, + final int 서포터의_리뷰수, + final String 지원한_서포터_어필_메시지, + final List 서포터_기술_태그_목록 + ) { + return new SupporterRunnerPostResponse.Detail( + 지원한_서포터.getId(), + 지원한_서포터.getMember().getMemberName().getValue(), + 지원한_서포터.getMember().getCompany().getValue(), + 서포터의_리뷰수, + 지원한_서포터.getMember().getImageUrl().getValue(), + 지원한_서포터_어필_메시지, + 서포터_기술_태그_목록 + ); + } + + public static SupporterRunnerPostResponses.Detail 지원한_서포터_응답_목록_응답(final List 지원한_서포터_응답_목록) { + return SupporterRunnerPostResponses.Detail.from(지원한_서포터_응답_목록); + } + public static class RunnerPostClientRequestBuilder { private ExtractableResponse response; private String accessToken; - public RunnerPostClientRequestBuilder 토큰으로_로그인한다(final String 토큰) { - this.accessToken = 토큰; + public RunnerPostClientRequestBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; return this; } public RunnerPostClientRequestBuilder 러너_게시글_등록_요청한다(final RunnerPostCreateRequest 게시글_생성_요청) { - response = AssuredSupport.post("/api/v1/posts/runner", 게시글_생성_요청, accessToken); + response = AssuredSupport.post("/api/v1/posts/runner", accessToken, 게시글_생성_요청); return this; } public RunnerPostClientRequestBuilder 러너_게시글_식별자값으로_러너_게시글을_조회한다(final Long 러너_게시글_식별자값) { - response = AssuredSupport.get("/api/v1/posts/runner/{runnerPostId}", "runnerPostId", 러너_게시글_식별자값, accessToken); + response = AssuredSupport.get( + "/api/v1/posts/runner/{runnerPostId}", + accessToken, + new PathParams(Map.of("runnerPostId", 러너_게시글_식별자값))); return this; } - public RunnerPostClientRequestBuilder 러너_게시글_식별자값으로_서포터_러너_게시글을_조회한다(final Long 러너_게시글_식별자값) { - response = AssuredSupport.get("/api/v1/posts/runner/{runnerPostId}/supporters", "runnerPostId", 러너_게시글_식별자값, accessToken); + public RunnerPostClientRequestBuilder 러너_게시글_식별자값으로_지원한_서포터_목록을_조회한다(final Long 러너_게시글_식별자값) { + response = AssuredSupport.get( + "/api/v1/posts/runner/{runnerPostId}/supporters", + accessToken, + new PathParams(Map.of("runnerPostId", 러너_게시글_식별자값))); return this; } @@ -130,40 +224,51 @@ public static class RunnerPostClientRequestBuilder { "page", 페이징_정보.getPageNumber() ); - response = AssuredSupport.get("/api/v1/posts/runner/search", queryParams); + response = AssuredSupport.get("/api/v1/posts/runner/search", new QueryParams(queryParams)); return this; } - public RunnerPostClientRequestBuilder 마이페이지_러너_게시글_페이징을_조회한다(final ReviewStatus 리뷰_상태, - final Pageable 페이징_정보 - ) { + public RunnerPostClientRequestBuilder 마이페이지_러너_게시글_페이징을_조회한다(final ReviewStatus 리뷰_상태, final Pageable 페이징_정보) { final Map queryParams = Map.of( "reviewStatus", 리뷰_상태, "size", 페이징_정보.getPageSize(), "page", 페이징_정보.getPageNumber() ); - response = AssuredSupport.get("/api/v1/posts/runner/me/runner", queryParams, accessToken); + response = AssuredSupport.get("/api/v1/posts/runner/me/runner", accessToken, new QueryParams(queryParams)); return this; } - public RunnerPostClientRequestBuilder 전체_러너_게시글_페이징을_조회한다(final Pageable 페이징_정보) { + public RunnerPostClientRequestBuilder 리뷰_상태로_전체_러너_게시글_페이징을_조회한다(final Pageable 페이징_정보, final ReviewStatus 리뷰_상태) { final Map queryParams = Map.of( "size", 페이징_정보.getPageSize(), - "page", 페이징_정보.getPageNumber() + "page", 페이징_정보.getPageNumber(), + "reviewStatus", 리뷰_상태 ); - response = AssuredSupport.get("/api/v1/posts/runner", queryParams); + response = AssuredSupport.get("/api/v1/posts/runner", new QueryParams(queryParams)); + return this; + } + + public RunnerPostClientRequestBuilder 태그_이름과_리뷰_상태를_조건으로_러너_게시글_페이징을_조회한다(final Pageable 페이징_정보, final String 태그_이름, final ReviewStatus 리뷰_상태) { + final Map queryParams = Map.of( + "size", 페이징_정보.getPageSize(), + "page", 페이징_정보.getPageNumber(), + "tagName", 태그_이름, + "reviewStatus", 리뷰_상태 + ); + + response = AssuredSupport.get("/api/v1/posts/runner/tags/search", new QueryParams(queryParams)); return this; } public RunnerPostClientRequestBuilder 서포터가_리뷰를_완료하고_리뷰완료_버튼을_누른다(final Long 게시글_식별자) { - response = AssuredSupport.patch("/api/v1/posts/runner/{runnerPostId}/done", "runnerPostId", 게시글_식별자, accessToken); + response = AssuredSupport.patch("/api/v1/posts/runner/{runnerPostId}/done", accessToken, new PathParams(Map.of("runnerPostId", 게시글_식별자))); return this; } public RunnerPostClientRequestBuilder 러너_게시글_식별자값으로_러너_게시글을_삭제한다(final Long 러너_게시글_식별자값) { - response = AssuredSupport.delete("/api/v1/posts/runner/{runnerPostId}", accessToken, "runnerPostId", 러너_게시글_식별자값); + response = AssuredSupport.delete("/api/v1/posts/runner/{runnerPostId}", accessToken, new PathParams(Map.of("runnerPostId", 러너_게시글_식별자값))); return this; } @@ -171,9 +276,9 @@ public static class RunnerPostClientRequestBuilder { final RunnerPostUpdateRequest.SelectSupporter 서포터_선택_요청_정보 ) { response = AssuredSupport.patch("/api/v1/posts/runner/{runnerPostId}/supporters", - "runnerPostId", 게시글_식별자값, - 서포터_선택_요청_정보, - accessToken + accessToken, + new PathParams(Map.of("runnerPostId", 게시글_식별자값)), + 서포터_선택_요청_정보 ); return this; } @@ -187,7 +292,7 @@ public static class RunnerPostClientRequestBuilder { "page", 페이징_정보.getPageNumber() ); - response = AssuredSupport.get("/api/v1/posts/runner/me/supporter", accessToken, queryParams); + response = AssuredSupport.get("/api/v1/posts/runner/me/supporter", accessToken, new QueryParams(queryParams)); return this; } @@ -210,7 +315,9 @@ public RunnerPostServerResponseBuilder(final ExtractableResponse respo assertSoftly(softly -> { softly.assertThat(actual.runnerPostId()).isEqualTo(러너_게시글_응답.runnerPostId()); softly.assertThat(actual.title()).isEqualTo(러너_게시글_응답.title()); - softly.assertThat(actual.contents()).isEqualTo(러너_게시글_응답.contents()); + softly.assertThat(actual.implementedContents()).isEqualTo(러너_게시글_응답.implementedContents()); + softly.assertThat(actual.curiousContents()).isEqualTo(러너_게시글_응답.curiousContents()); + softly.assertThat(actual.postscriptContents()).isEqualTo(러너_게시글_응답.postscriptContents()); softly.assertThat(actual.deadline()).isEqualToIgnoringSeconds(러너_게시글_응답.deadline()); softly.assertThat(actual.watchedCount()).isEqualTo(러너_게시글_응답.watchedCount()); softly.assertThat(actual.applicantCount()).isEqualTo(러너_게시글_응답.applicantCount()); @@ -228,7 +335,7 @@ public RunnerPostServerResponseBuilder(final ExtractableResponse respo } public void 마이페이지_러너_게시글_페이징_조회_성공을_검증한다(final PageResponse 마이페이지_러너_게시글_페이징_응답) { - final PageResponse actual = this.response.as(new TypeRef>() { + final PageResponse actual = this.response.as(new TypeRef<>() { }); @@ -240,7 +347,7 @@ public RunnerPostServerResponseBuilder(final ExtractableResponse respo } public void 서포터와_연관된_러너_게시글_페이징_조회_성공을_검증한다(final PageResponse 서포터와_연관된_러너_게시글_페이징_응답) { - final PageResponse actual = this.response.as(new TypeRef>() { + final PageResponse actual = this.response.as(new TypeRef<>() { }); @@ -252,7 +359,7 @@ public RunnerPostServerResponseBuilder(final ExtractableResponse respo } public void 전체_러너_게시글_페이징_조회_성공을_검증한다(final PageResponse 전체_러너_게시글_페이징_응답) { - final PageResponse actual = this.response.as(new TypeRef>() { + final PageResponse actual = this.response.as(new TypeRef<>() { }); @@ -263,8 +370,8 @@ public RunnerPostServerResponseBuilder(final ExtractableResponse respo ); } - public void 서포터_러너_게시글_조회_성공을_검증한다(final SupporterRunnerPostResponses.Detail 전체_러너_게시글_페이징_응답) { - final SupporterRunnerPostResponses.Detail actual = this.response.as(new TypeRef() { + public void 지원한_서포터_목록_조회_성공을_검증한다(final SupporterRunnerPostResponses.Detail 전체_러너_게시글_페이징_응답) { + final SupporterRunnerPostResponses.Detail actual = this.response.as(new TypeRef<>() { }); @@ -275,6 +382,17 @@ public RunnerPostServerResponseBuilder(final ExtractableResponse respo ); } + public void 태그_이름과_리뷰_상태를_조건으로_러너_게시글_페이징_조회_성공을_검증한다(final PageResponse 러너_게시글_페이징_응답) { + final PageResponse actual = this.response.as(new TypeRef<>() { + }); + + assertSoftly(softly -> { + softly.assertThat(this.response.statusCode()).isEqualTo(HttpStatus.OK.value()); + softly.assertThat(actual.data()).isEqualTo(러너_게시글_페이징_응답.data()); + } + ); + } + public void 러너_게시글_삭제_성공을_검증한다(final HttpStatus HTTP_STATUS) { assertThat(response.statusCode()).isEqualTo(HTTP_STATUS.value()); } diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostCreateAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostCreateAssuredTest.java index 9e8104782..f40df0508 100644 --- a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostCreateAssuredTest.java +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostCreateAssuredTest.java @@ -1,14 +1,11 @@ package touch.baton.assure.runnerpost; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import touch.baton.assure.common.HttpStatusAndLocationHeader; import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.MockAuthCodes; import touch.baton.domain.common.response.ErrorResponse; -import touch.baton.domain.member.Member; import touch.baton.domain.runnerpost.service.dto.RunnerPostCreateRequest; -import touch.baton.fixture.domain.MemberFixture; -import touch.baton.fixture.domain.RunnerFixture; import java.time.LocalDateTime; import java.util.List; @@ -18,30 +15,24 @@ @SuppressWarnings("NonAsciiCharacters") class RunnerPostCreateAssuredTest extends AssuredTestConfig { - private static String 토큰; - - @BeforeEach - void setUp() { - final String 소셜_아이디 = "hongSile"; - final Member 사용자 = memberRepository.save(MemberFixture.createWithSocialId(소셜_아이디)); - runnerRepository.save(RunnerFixture.createRunner(사용자)); - 토큰 = login(소셜_아이디); - } - @Test void 러너_게시글_등록이_성공한다() { // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest("코드 리뷰 해주세요.", List.of("Java", "Spring"), "https://github.com/cookienc", LocalDateTime.now().plusDays(10), - "싸게 부탁드려요." + "싸게 부탁드려요.", + "이거 궁금해요.", + "잘 부탁드립니다." ); // when, then RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(토큰) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) .러너_게시글_등록_요청한다(게시글_생성_요청) .서버_응답() @@ -51,17 +42,21 @@ void setUp() { @Test void 게시글_제목이_null이면_러너_게시글_등록_실패한다() { // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest(null, List.of("Java", "Spring"), "https://github.com/cookienc", LocalDateTime.now().plusDays(10), - "싸게 부탁드려요." + "싸게 부탁드려요.", + "이거 궁금해요.", + "잘 부탁드립니다." ); // when, then RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(토큰) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) .러너_게시글_등록_요청한다(게시글_생성_요청) .서버_응답() @@ -71,17 +66,21 @@ void setUp() { @Test void 게시글_태그가_null이면_러너_게시글_등록_실패한다() { // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest("코드 리뷰 해주세요.", null, "https://github.com/cookienc", LocalDateTime.now().plusDays(10), - "싸게 부탁드려요." + "싸게 부탁드려요.", + "이거 궁금해요.", + "잘 부탁드립니다." ); // when, then RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(토큰) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) .러너_게시글_등록_요청한다(게시글_생성_요청) .서버_응답() @@ -91,17 +90,21 @@ void setUp() { @Test void 게시글_PR_URL이_null이면_러너_게시글_등록_실패한다() { // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest("코드 리뷰 해주세요.", List.of("Java", "Spring"), null, LocalDateTime.now().plusDays(10), - "싸게 부탁드려요." + "싸게 부탁드려요.", + "이거 궁금해요.", + "잘 부탁드립니다." ); // when, then RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(토큰) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) .러너_게시글_등록_요청한다(게시글_생성_요청) .서버_응답() @@ -111,17 +114,21 @@ void setUp() { @Test void 게시글_마감기한이_null이면_러너_게시글_등록_실패한다() { // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest("코드 리뷰 해주세요.", List.of("Java", "Spring"), "https://github.com/cookienc", null, - "싸게 부탁드려요." + "싸게 부탁드려요.", + "이거 궁금해요.", + "잘 부탁드립니다." ); // when, then RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(토큰) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) .러너_게시글_등록_요청한다(게시글_생성_요청) .서버_응답() @@ -131,17 +138,21 @@ void setUp() { @Test void 게시글_마감기한이_현재보다_과거면_러너_게시글_등록_실패한다() { // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest("코드 리뷰 해주세요.", List.of("Java", "Spring"), "https://github.com/cookienc", LocalDateTime.now().minusDays(1), - "싸게 부탁드려요." + "싸게 부탁드려요.", + "이거 궁금해요.", + "잘 부탁드립니다." ); // when, then RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(토큰) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) .러너_게시글_등록_요청한다(게시글_생성_요청) .서버_응답() @@ -149,39 +160,95 @@ void setUp() { } @Test - void 게시글_내용이_null이면_러너_게시글_등록_실패한다() { + void 구현_내용이_null이면_러너_게시글_등록_실패한다() { // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest("코드 리뷰 해주세요.", List.of("Java", "Spring"), "https://github.com/cookienc", LocalDateTime.now().plusDays(10), + null, + "이거 궁금해요.", + "잘 부탁드립니다." + ); + + // when, then + RunnerPostAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다(게시글_생성_요청) + + .서버_응답() + .러너_게시글_등록_실패를_검증한다(new ErrorResponse("RP004", "구현 내용을 입력해주세요.")); + } + + @Test + void 궁금한_내용이_null이면_러너_게시글_등록_실패한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest("코드 리뷰 해주세요.", + List.of("Java", "Spring"), + "https://github.com/cookienc", + LocalDateTime.now().plusDays(10), + "이거 해줘요.", + null, + "잘 부탁드립니다." + ); + + // when, then + RunnerPostAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다(게시글_생성_요청) + + .서버_응답() + .러너_게시글_등록_실패를_검증한다(new ErrorResponse("RP012", "궁금한 내용을 입력해주세요.")); + } + + @Test + void 참고_사항이_null이면_러너_게시글_등록_실패한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest("코드 리뷰 해주세요.", + List.of("Java", "Spring"), + "https://github.com/cookienc", + LocalDateTime.now().plusDays(10), + "잘 부탁드립니다.", + "이거 궁금해요.", null ); // when, then RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(토큰) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) .러너_게시글_등록_요청한다(게시글_생성_요청) .서버_응답() - .러너_게시글_등록_실패를_검증한다(new ErrorResponse("RP004", "내용을 입력해주세요.")); + .러너_게시글_등록_실패를_검증한다(new ErrorResponse("RP013", "참고 사항을 입력해주세요.")); } @Test void 게시글_내용이_1000자_보다_길면_러너_게시글_등록_실패한다() { // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest("코드 리뷰 해주세요.", List.of("Java", "Spring"), "https://github.com/cookienc", LocalDateTime.now().plusDays(10), - "12345".repeat(200) + "1" + "12345".repeat(200) + "1", + "", + "" ); // when, then RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(토큰) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) .러너_게시글_등록_요청한다(게시글_생성_요청) .서버_응답() diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostDeleteAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostDeleteAssuredTest.java index 0ab00926f..84f291332 100644 --- a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostDeleteAssuredTest.java +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostDeleteAssuredTest.java @@ -1,46 +1,35 @@ package touch.baton.assure.runnerpost; -import org.junit.jupiter.api.BeforeEach; 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.vo.ReviewStatus; +import touch.baton.config.infra.auth.oauth.authcode.MockAuthCodes; +import touch.baton.domain.member.vo.SocialId; 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 java.util.List; import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; import static org.springframework.http.HttpStatus.NO_CONTENT; -import static touch.baton.fixture.vo.DeadlineFixture.deadline; +import static touch.baton.assure.runnerpost.RunnerPostAssuredCreateSupport.러너_게시글_생성_요청; +import static touch.baton.assure.runnerpost.RunnerPostAssuredSupport.러너의_서포터_선택_요청; @SuppressWarnings("NonAsciiCharacters") class RunnerPostDeleteAssuredTest extends AssuredTestConfig { - private Runner 러너_디투; - private String 로그인용_토큰; - - @BeforeEach - void setUp() { - final Member 사용자_디투 = memberRepository.save(MemberFixture.createDitoo()); - 러너_디투 = runnerRepository.save(RunnerFixture.createRunner(사용자_디투)); - 로그인용_토큰 = login(사용자_디투.getSocialId().getValue()); - } - @Test void 리뷰가_대기중이고_리뷰_지원자가_없다면_러너의_게시글_식별자값으로_러너_게시글_삭제에_성공한다() { - final RunnerPost 러너_게시글 = runnerPostRepository.save(RunnerPostFixture.create(러너_디투, deadline(LocalDateTime.now().plusHours(100)))); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final Long 러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(헤나_액세스_토큰); + // when, then RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(로그인용_토큰) - .러너_게시글_식별자값으로_러너_게시글을_삭제한다(러너_게시글.getId()) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_식별자값으로_러너_게시글을_삭제한다(러너_게시글_식별자값) .서버_응답() .러너_게시글_삭제_성공을_검증한다(NO_CONTENT); @@ -48,11 +37,16 @@ void setUp() { @Test void 러너_게시글이_존재하지_않으면_러너의_게시글_식별자값으로_러너_게시글_삭제에_실패한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + + // when final Long 존재하지_않는_러너_게시글의_식별자값 = -1L; + // then RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(로그인용_토큰) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) .러너_게시글_식별자값으로_러너_게시글을_삭제한다(존재하지_않는_러너_게시글의_식별자값) .서버_응답() @@ -61,16 +55,19 @@ void setUp() { @Test void 리뷰가_진행중인_상태라면_러너의_게시글_식별자값으로_러너_게시글_삭제에_실패한다() { - final RunnerPost 러너_게시글 = runnerPostRepository.save(RunnerPostFixture.create( - 러너_디투, - deadline(LocalDateTime.now().plusHours(100)), - ReviewStatus.IN_PROGRESS - )); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + + // then RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(로그인용_토큰) - .러너_게시글_식별자값으로_러너_게시글을_삭제한다(러너_게시글.getId()) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_식별자값으로_러너_게시글을_삭제한다(디투_러너_게시글_식별자값) .서버_응답() .러너_게시글_삭제_실패를_검증한다(INTERNAL_SERVER_ERROR); @@ -78,16 +75,24 @@ void setUp() { @Test void 리뷰가_완료된_상태라면_러너의_게시글_식별자값으로_러너_게시글_삭제에_실패한다() { - final RunnerPost 러너_게시글 = runnerPostRepository.save(RunnerPostFixture.create( - 러너_디투, - deadline(LocalDateTime.now().plusHours(100)), - ReviewStatus.DONE - )); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Supporter 서포터_헤나 = supporterRepository.getBySocialId(헤나_소셜_아이디); + + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + 러너가_서포터의_리뷰_신청_선택에_성공한다(서포터_헤나, 디투_액세스_토큰, 디투_러너_게시글_식별자값); + 서포터가_러너_게시글의_리뷰를_완료로_변경하는_것을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + + // then RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(로그인용_토큰) - .러너_게시글_식별자값으로_러너_게시글을_삭제한다(러너_게시글.getId()) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_식별자값으로_러너_게시글을_삭제한다(디투_러너_게시글_식별자값) .서버_응답() .러너_게시글_삭제_실패를_검증한다(INTERNAL_SERVER_ERROR); @@ -95,21 +100,72 @@ void setUp() { @Test void 리뷰_요청_대기중인_상태이고_리뷰_지원자가_있는_상태라면_러너의_게시글_식별자값으로_러너_게시글_삭제에_실패한다() { - final RunnerPost 러너_게시글 = runnerPostRepository.save(RunnerPostFixture.create( - 러너_디투, - deadline(LocalDateTime.now().plusHours(100)), - ReviewStatus.NOT_STARTED - )); - final Member 지원자_맴버 = memberRepository.save(MemberFixture.createHyena()); - final Supporter 지원자_서포터 = supporterRepository.save(SupporterFixture.create(지원자_맴버)); - supporterRunnerPostRepository.save(SupporterRunnerPostFixture.create(러너_게시글, 지원자_서포터)); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + // then RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(로그인용_토큰) - .러너_게시글_식별자값으로_러너_게시글을_삭제한다(러너_게시글.getId()) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_식별자값으로_러너_게시글을_삭제한다(디투_러너_게시글_식별자값) .서버_응답() .러너_게시글_삭제_실패를_검증한다(INTERNAL_SERVER_ERROR); } + + private Long 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(final String 헤나_액세스_토큰) { + return RunnerPostAssuredCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너가_러너_게시글을_작성한다( + 러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + List.of("자바", "스프링"), + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + ) + ) + + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); + } + + private void 서포터가_러너_게시글에_리뷰_신청을_성공한다(final String 헤나_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + RunnerPostAssuredCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .서포터가_러너_게시글에_리뷰를_신청한다(디투_러너_게시글_식별자값, "안녕하세요. 서포터 헤나입니다.") + + .서버_응답() + .서포터가_러너_게시글에_리뷰_신청_성공을_검증한다(디투_러너_게시글_식별자값); + } + + private void 러너가_서포터의_리뷰_신청_선택에_성공한다(final Supporter 서포터_헤나, final String 디투_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + RunnerPostAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(디투_액세스_토큰) + .러너가_서포터를_선택한다(디투_러너_게시글_식별자값, 러너의_서포터_선택_요청(서포터_헤나.getId())) + + .서버_응답() + .러너_게시글에_서포터가_성공적으로_선택되었는지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); + } + + private void 서포터가_러너_게시글의_리뷰를_완료로_변경하는_것을_성공한다(final String 헤나_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + RunnerPostAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .서포터가_리뷰를_완료하고_리뷰완료_버튼을_누른다(디투_러너_게시글_식별자값) + + .서버_응답() + .러너_게시글이_성공적으로_리뷰_완료_상태인지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); + } } diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostReadAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostReadAssuredTest.java index 88cbe06b6..16de0c0f0 100644 --- a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostReadAssuredTest.java +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostReadAssuredTest.java @@ -3,80 +3,123 @@ import org.junit.jupiter.api.Test; import org.springframework.data.domain.PageRequest; import touch.baton.config.AssuredTestConfig; -import touch.baton.domain.common.response.PageResponse; -import touch.baton.domain.member.Member; -import touch.baton.domain.runner.Runner; +import touch.baton.config.infra.auth.oauth.authcode.MockAuthCodes; import touch.baton.domain.runnerpost.RunnerPost; import touch.baton.domain.runnerpost.controller.response.RunnerPostResponse; import touch.baton.domain.runnerpost.vo.ReviewStatus; -import touch.baton.fixture.domain.MemberFixture; -import touch.baton.fixture.domain.RunnerFixture; -import touch.baton.fixture.domain.RunnerPostFixture; +import java.time.LocalDateTime; import java.util.List; -import static java.time.LocalDateTime.now; -import static touch.baton.assure.runnerpost.RunnerPostAssuredSupport.러너_게시글_전체_조회_응답; -import static touch.baton.assure.runnerpost.RunnerPostAssuredSupport.마이페이지_러너_게시글_응답; -import static touch.baton.fixture.vo.DeadlineFixture.deadline; -import static touch.baton.fixture.vo.IntroductionFixture.introduction; +import static touch.baton.assure.runnerpost.RunnerPostAssuredCreateSupport.러너_게시글_생성_요청; +import static touch.baton.assure.runnerpost.RunnerPostAssuredSupport.*; @SuppressWarnings("NonAsciiCharacters") class RunnerPostReadAssuredTest extends AssuredTestConfig { @Test void 러너_게시글_전체_조회에_성공한다() { - final Runner 러너_에단 = 러너를_저장한다(MemberFixture.createEthan()); - final RunnerPost 러너_에단의_게시글 = 러너_게시글을_등록한다(러너_에단); - runnerPostRepository.save(러너_에단의_게시글); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); - final String 에단_액세스_토큰 = login(러너_에단.getMember().getSocialId().getValue()); + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + + final RunnerPost 디투_러너_게시글 = runnerPostRepository.getByRunnerPostId(디투_러너_게시글_식별자값); + final Long 디투_러너_게시글에_지원한_서포터_수 = supporterRunnerPostRepository.getApplicantCountByRunnerPostId(디투_러너_게시글_식별자값); final PageRequest 페이징_정보 = PageRequest.of(1, 10); - final RunnerPostResponse.Simple 게시글_응답 - = RunnerPostResponse.Simple.from(러너_에단의_게시글, 0); - final PageResponse 페이징된_게시글_응답 - = 러너_게시글_전체_조회_응답(페이징_정보, List.of(게시글_응답)); + final RunnerPostResponse.Simple 디투_러너_게시글_Simple_응답 = 러너_게시글_Simple_응답(디투_러너_게시글, 0, 디투_러너_게시글에_지원한_서포터_수, ReviewStatus.NOT_STARTED, List.of("자바", "스프링")); + // when, then RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(에단_액세스_토큰) - .전체_러너_게시글_페이징을_조회한다(페이징_정보) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .리뷰_상태로_전체_러너_게시글_페이징을_조회한다(페이징_정보, ReviewStatus.NOT_STARTED) .서버_응답() - .전체_러너_게시글_페이징_조회_성공을_검증한다(페이징된_게시글_응답); + .전체_러너_게시글_페이징_조회_성공을_검증한다( + 러너_게시글_전체_Simple_페이징_응답(페이징_정보, List.of(디투_러너_게시글_Simple_응답)) + ); } @Test void 마이페이지_러너_게시글_페이징_조회에_성공한다() { - final Member 멤버_에단 = MemberFixture.createEthan(); - final Runner 러너_에단 = 러너를_저장한다(멤버_에단); - final RunnerPost 러너_에단의_게시글 = 러너_게시글을_등록한다(러너_에단); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final Long 헤나_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(헤나_액세스_토큰); - final String 에단_액세스_토큰 = login(러너_에단.getMember().getSocialId().getValue()); + final RunnerPost 헤나_러너_게시글 = runnerPostRepository.getByRunnerPostId(헤나_러너_게시글_식별자값); + final Long 헤나_러너_게시글에_지원한_서포터_수 = supporterRunnerPostRepository.getApplicantCountByRunnerPostId(헤나_러너_게시글_식별자값); final PageRequest 페이징_정보 = PageRequest.of(1, 10); - final RunnerPostResponse.SimpleInMyPage 마이페이지_러너_게시글_응답 - = RunnerPostResponse.SimpleInMyPage.from(러너_에단의_게시글, 0); - final PageResponse 페이징된_마이페이지_러너_게시글_응답 - = 마이페이지_러너_게시글_응답(페이징_정보, List.of(마이페이지_러너_게시글_응답)); + // when, then RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(에단_액세스_토큰) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) .마이페이지_러너_게시글_페이징을_조회한다(ReviewStatus.NOT_STARTED, 페이징_정보) .서버_응답() - .마이페이지_러너_게시글_페이징_조회_성공을_검증한다(페이징된_마이페이지_러너_게시글_응답); + .마이페이지_러너_게시글_페이징_조회_성공을_검증한다( + 마이페이지_러너_게시글_응답(페이징_정보, + List.of( + 마이페이지_러너_게시글_SimpleInMyPage_응답(헤나_러너_게시글, null, 0, 헤나_러너_게시글에_지원한_서포터_수, ReviewStatus.NOT_STARTED, List.of("자바", "스프링") + ) + )) + ); } - private RunnerPost 러너_게시글을_등록한다(final Runner 러너) { - return runnerPostRepository.save(RunnerPostFixture.create(러너, deadline(now().plusHours(100)))); + @Test + void 태그_이름과_리뷰_상태를_조건으로_러너_게시글_페이징을_조회에_성공한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + + final Long 헤나_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(헤나_액세스_토큰); + + final RunnerPost 헤나_러너_게시글 = runnerPostRepository.getByRunnerPostId(헤나_러너_게시글_식별자값); + final Long 서포터_지원자_수 = runnerPostReadRepository.countApplicantByRunnerPostId(헤나_러너_게시글_식별자값); + + final PageRequest 페이징_정보 = PageRequest.of(1, 10); + + // when, then + final RunnerPostResponse.Simple 기대된_헤나_러너_게시글_Simple_응답 = 러너_게시글_Simple_응답( + 헤나_러너_게시글, + 0, + 서포터_지원자_수, + ReviewStatus.NOT_STARTED, + List.of("자바", "스프링") + ); + + RunnerPostAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .태그_이름과_리뷰_상태를_조건으로_러너_게시글_페이징을_조회한다(페이징_정보, "자바", ReviewStatus.NOT_STARTED) + + .서버_응답() + .태그_이름과_리뷰_상태를_조건으로_러너_게시글_페이징_조회_성공을_검증한다( + 러너_게시글_전체_Simple_페이징_응답(페이징_정보, List.of(기대된_헤나_러너_게시글_Simple_응답)) + ); } - private Runner 러너를_저장한다(final Member 사용자) { - final Member 저장된_사용자 = memberRepository.save(사용자); + private Long 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(final String 사용자_액세스_토큰) { + return RunnerPostAssuredCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(사용자_액세스_토큰) + .러너가_러너_게시글을_작성한다( + 러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + List.of("자바", "스프링"), + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + ) + ) - return runnerRepository.save(RunnerFixture.createRunner(introduction("안녕하세요"), 저장된_사용자)); + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); } } diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostReadByRunnerPostIdAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostReadByRunnerPostIdAssuredTest.java index cd8d070f9..6de97d797 100644 --- a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostReadByRunnerPostIdAssuredTest.java +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostReadByRunnerPostIdAssuredTest.java @@ -2,21 +2,17 @@ import org.junit.jupiter.api.Test; import touch.baton.config.AssuredTestConfig; -import touch.baton.domain.member.Member; +import touch.baton.config.infra.auth.oauth.authcode.MockAuthCodes; +import touch.baton.domain.member.vo.SocialId; import touch.baton.domain.runner.Runner; -import touch.baton.domain.runner.controller.response.RunnerResponse; import touch.baton.domain.runnerpost.RunnerPost; import touch.baton.domain.runnerpost.controller.response.RunnerPostResponse; -import touch.baton.domain.runnerpost.vo.ReviewStatus; -import touch.baton.fixture.domain.MemberFixture; -import touch.baton.fixture.domain.RunnerFixture; -import touch.baton.fixture.domain.RunnerPostFixture; import java.time.LocalDateTime; -import java.util.ArrayList; +import java.util.List; -import static touch.baton.fixture.vo.DeadlineFixture.deadline; -import static touch.baton.fixture.vo.IntroductionFixture.introduction; +import static touch.baton.assure.runnerpost.RunnerPostAssuredCreateSupport.러너_게시글_생성_요청; +import static touch.baton.assure.runnerpost.RunnerPostAssuredSupport.러너_게시글_Detail_응답; import static touch.baton.fixture.vo.WatchedCountFixture.watchedCount; @SuppressWarnings("NonAsciiCharacters") @@ -24,30 +20,76 @@ class RunnerPostReadByRunnerPostIdAssuredTest extends AssuredTestConfig { @Test void 러너의_게시글_식별자값으로_러너_게시글_상세_정보_조회에_성공한다() { - final Member 사용자_헤나 = memberRepository.save(MemberFixture.createHyena()); - final Runner 러너_헤나 = runnerRepository.save(RunnerFixture.createRunner(introduction("안녕하세요"), 사용자_헤나)); - final RunnerPost 러너_게시글 = runnerPostRepository.save(RunnerPostFixture.create(러너_헤나, deadline(LocalDateTime.now().plusHours(100)))); - final String 로그인용_토큰 = login(사용자_헤나.getSocialId().getValue()); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Runner 러너_헤나 = runnerRepository.getBySocialId(헤나_소셜_아이디); + + final Long 헤나_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환에_성공한다(헤나_액세스_토큰); + final RunnerPost 헤나_러너_게시글 = runnerPostRepository.getByRunnerPostId(헤나_러너_게시글_식별자값); + + final RunnerPostResponse.Detail 러너_게시글_detail_응답 = 러너_게시글_Detail_응답을_생성한다( + 러너_헤나, 헤나_러너_게시글, + watchedCount(1).getValue(), + 0, + false, + List.of("자바", "스프링") + ); + + // when, then RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(로그인용_토큰) - .러너_게시글_식별자값으로_러너_게시글을_조회한다(러너_게시글.getId()) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_식별자값으로_러너_게시글을_조회한다(헤나_러너_게시글_식별자값) + + .서버_응답() + .러너_게시글_단건_조회_성공을_검증한다(러너_게시글_detail_응답); + } + + private Long 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환에_성공한다(final String 헤나_액세스_토큰) { + return RunnerPostAssuredCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너가_러너_게시글을_작성한다( + 러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + List.of("자바", "스프링"), + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + ) + ) .서버_응답() - .러너_게시글_단건_조회_성공을_검증한다(new RunnerPostResponse.Detail( - 러너_게시글.getId(), - 러너_게시글.getTitle().getValue(), - 러너_게시글.getContents().getValue(), - 러너_게시글.getPullRequestUrl().getValue(), - 러너_게시글.getDeadline().getValue(), - watchedCount(1).getValue(), - 0L, - ReviewStatus.NOT_STARTED, - true, - false, - new ArrayList<>(), - RunnerResponse.Detail.from(러너_헤나) - )); + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); + } + + private RunnerPostResponse.Detail 러너_게시글_Detail_응답을_생성한다(final Runner 작성자_러너, + final RunnerPost 러너_게시글, + final int 조회수, + final long 서포터_지원자수, + final boolean 서포터_지원_여부, + final List 러너_게시글_태그_목록 + ) { + return 러너_게시글_Detail_응답( + 러너_게시글.getId(), + 러너_게시글.getTitle().getValue(), + 러너_게시글.getImplementedContents().getValue(), + 러너_게시글.getCuriousContents().getValue(), + 러너_게시글.getPostscriptContents().getValue(), + 러너_게시글.getPullRequestUrl().getValue(), + 러너_게시글.getDeadline().getValue(), + 조회수, + 서포터_지원자수, + 러너_게시글.getReviewStatus(), + !러너_게시글.isNotOwner(작성자_러너), + 서포터_지원_여부, + 러너_게시글.getRunner(), + 러너_게시글_태그_목록 + ); } } diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostReadWithLoginedAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostReadWithLoginedAssuredTest.java index d55c42cef..31b1c3a81 100644 --- a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostReadWithLoginedAssuredTest.java +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostReadWithLoginedAssuredTest.java @@ -2,96 +2,80 @@ import org.junit.jupiter.api.Test; 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.controller.response.RunnerPostResponse; +import touch.baton.config.infra.auth.oauth.authcode.MockAuthCodes; +import touch.baton.domain.member.vo.SocialId; import touch.baton.domain.runnerpost.controller.response.SupporterRunnerPostResponse; -import touch.baton.domain.runnerpost.controller.response.SupporterRunnerPostResponses; -import touch.baton.domain.runnerpost.vo.ReviewStatus; import touch.baton.domain.supporter.Supporter; -import touch.baton.domain.supporter.SupporterRunnerPost; -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 java.util.Collections; import java.util.List; -import static java.time.LocalDateTime.now; -import static touch.baton.assure.runnerpost.RunnerPostAssuredSupport.러너_게시글_Detail_응답; -import static touch.baton.assure.runnerpost.RunnerPostAssuredSupport.클라이언트_요청; -import static touch.baton.fixture.vo.DeadlineFixture.deadline; -import static touch.baton.fixture.vo.IntroductionFixture.introduction; +import static touch.baton.assure.runnerpost.RunnerPostAssuredCreateSupport.러너_게시글_생성_요청; +import static touch.baton.assure.runnerpost.RunnerPostAssuredSupport.지원한_서포터_응답; +import static touch.baton.assure.runnerpost.RunnerPostAssuredSupport.지원한_서포터_응답_목록_응답; @SuppressWarnings("NonAsciiCharacters") class RunnerPostReadWithLoginedAssuredTest extends AssuredTestConfig { @Test - void 러너의_게시글_식별자값으로_러너_게시글_상세_정보_조회에_성공한다() { - final Member 사용자_헤나 = memberRepository.save(MemberFixture.createHyena()); - final Runner 러너_헤나 = runnerRepository.save(RunnerFixture.createRunner(introduction("안녕하세요"), 사용자_헤나)); - final RunnerPost 러너_게시글 = runnerPostRepository.save(RunnerPostFixture.create(러너_헤나, deadline(now().plusHours(100)))); - final String 로그인용_토큰 = login(사용자_헤나.getSocialId().getValue()); + void 러너의_게시글_식별자값으로_지원한_서포터_목록_조회에_성공한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); - final RunnerPostResponse.Detail 러너_게시글_detail_응답 = 러너_게시글_Detail_응답을_생성한다(러너_헤나, 러너_게시글, ReviewStatus.NOT_STARTED, 러너_게시글.getId(), 1, 0, false); + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); - 클라이언트_요청() - .토큰으로_로그인한다(로그인용_토큰) - .러너_게시글_식별자값으로_러너_게시글을_조회한다(러너_게시글.getId()) + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Supporter 서포터_헤나 = supporterRepository.getBySocialId(헤나_소셜_아이디); - .서버_응답() - .러너_게시글_단건_조회_성공을_검증한다(러너_게시글_detail_응답); - } - - private RunnerPostResponse.Detail 러너_게시글_Detail_응답을_생성한다(final Runner 로그인한_러너, - final RunnerPost 러너_게시글, - final ReviewStatus 리뷰_상태, - final Long 러너_게시글_식별자값, - final int 조회수, - final long 서포터_지원자수, - final boolean 서포터_지원_여부 - ) { - return 러너_게시글_Detail_응답( - 러너_게시글_식별자값, - 러너_게시글.getTitle().getValue(), - 러너_게시글.getContents().getValue(), - 러너_게시글.getPullRequestUrl().getValue(), - 러너_게시글.getDeadline().getValue(), - 조회수, - 서포터_지원자수, - 리뷰_상태, - !러너_게시글.isNotOwner(로그인한_러너), - 서포터_지원_여부, - 러너_게시글.getRunner(), - 러너_게시글.getRunnerPostTags().getRunnerPostTags().stream() - .map(runnerPostTag -> runnerPostTag.getTag().getTagName().getValue()) - .toList() + final SupporterRunnerPostResponse.Detail 지원한_서포터_헤나_응답 = 지원한_서포터_응답( + 서포터_헤나, + 0, + "안녕하세요. 서포터 헤나입니다.", + Collections.emptyList() ); - } - @Test - void 러너의_게시글_식별자값으로_서포터_러너_게시글_조회에_성공한다() { - final Member 사용자_헤나 = memberRepository.save(MemberFixture.createHyena()); - final Runner 러너_헤나 = runnerRepository.save(RunnerFixture.createRunner(introduction("안녕하세요"), 사용자_헤나)); - final RunnerPost 러너_게시글 = runnerPostRepository.save(RunnerPostFixture.create(러너_헤나, deadline(LocalDateTime.now().plusHours(100)))); - final String 로그인용_토큰 = login(사용자_헤나.getSocialId().getValue()); + // then + RunnerPostAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(디투_액세스_토큰) + .러너_게시글_식별자값으로_지원한_서포터_목록을_조회한다(디투_러너_게시글_식별자값) - final Member 사용자_주디 = memberRepository.save(MemberFixture.createJudy()); - final Supporter 서포터_주디 = supporterRepository.save(SupporterFixture.create(사용자_주디)); - final SupporterRunnerPost 서포터_러너_게시글 = supporterRunnerPostRepository.save(SupporterRunnerPostFixture.create(러너_게시글, 서포터_주디)); + .서버_응답() + .지원한_서포터_목록_조회_성공을_검증한다(지원한_서포터_응답_목록_응답(List.of(지원한_서포터_헤나_응답))); + } - final SupporterRunnerPostResponse.Detail 서포터_러너_게시글_응답 = SupporterRunnerPostResponse.Detail.from(서포터_러너_게시글); - final SupporterRunnerPostResponses.Detail 서포터_러너_게시글_응답들 = SupporterRunnerPostResponses.Detail.from(List.of(서포터_러너_게시글_응답)); + private Long 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(final String 헤나_액세스_토큰) { + return RunnerPostAssuredCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너가_러너_게시글을_작성한다( + 러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + List.of("자바", "스프링"), + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + ) + ) - RunnerPostAssuredSupport + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); + } + + private void 서포터가_러너_게시글에_리뷰_신청을_성공한다(final String 서포터_액세스_토큰, final Long 러너_게시글_식별자값) { + RunnerPostAssuredCreateSupport .클라이언트_요청() - .토큰으로_로그인한다(로그인용_토큰) - .러너_게시글_식별자값으로_서포터_러너_게시글을_조회한다(러너_게시글.getId()) + .액세스_토큰으로_로그인한다(서포터_액세스_토큰) + .서포터가_러너_게시글에_리뷰를_신청한다(러너_게시글_식별자값, "안녕하세요. 서포터 헤나입니다.") .서버_응답() - .서포터_러너_게시글_조회_성공을_검증한다(서포터_러너_게시글_응답들); + .서포터가_러너_게시글에_리뷰_신청_성공을_검증한다(러너_게시글_식별자값); } } diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostReadWithLoginedSupporterAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostReadWithLoginedSupporterAssuredTest.java index 984595359..c93425542 100644 --- a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostReadWithLoginedSupporterAssuredTest.java +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostReadWithLoginedSupporterAssuredTest.java @@ -1,119 +1,185 @@ package touch.baton.assure.runnerpost; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpStatus; +import touch.baton.assure.common.HttpStatusAndLocationHeader; import touch.baton.config.AssuredTestConfig; -import touch.baton.domain.common.response.PageResponse; -import touch.baton.domain.member.Member; -import touch.baton.domain.runner.Runner; +import touch.baton.config.infra.auth.oauth.authcode.MockAuthCodes; +import touch.baton.domain.member.vo.SocialId; import touch.baton.domain.runnerpost.RunnerPost; import touch.baton.domain.runnerpost.controller.response.RunnerPostResponse; import touch.baton.domain.runnerpost.vo.ReviewStatus; 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 java.util.List; -import static touch.baton.assure.runnerpost.RunnerPostAssuredSupport.서포터가_리뷰_완료한_러너_게시글_응답; -import static touch.baton.fixture.vo.DeadlineFixture.deadline; +import static touch.baton.assure.runnerpost.RunnerPostAssuredCreateSupport.러너_게시글_생성_요청; +import static touch.baton.assure.runnerpost.RunnerPostAssuredSupport.*; @SuppressWarnings("NonAsciiCharacters") -public class RunnerPostReadWithLoginedSupporterAssuredTest extends AssuredTestConfig { +class RunnerPostReadWithLoginedSupporterAssuredTest extends AssuredTestConfig { private final PageRequest 페이징_정보 = PageRequest.of(1, 10); - private Supporter 로그인된_서포터; - private String 로그인용_토큰; - private RunnerPost 대기중인_게시글; - private RunnerPost 리뷰중인_게시글; - private RunnerPost 완료된_게시글; - - @BeforeEach - void setUp() { - final Member 로그인된_사용자 = memberRepository.save(MemberFixture.createDitoo()); - 로그인된_서포터 = supporterRepository.save(SupporterFixture.create(로그인된_사용자)); - 로그인용_토큰 = login(로그인된_사용자.getSocialId().getValue()); - 로그인된_서포터의_러너_게시글을_모든_리뷰_상태로_저장한다(로그인된_서포터); - } - private void 로그인된_서포터의_러너_게시글을_모든_리뷰_상태로_저장한다(final Supporter 로그인된_서포터) { - final Member 사용자_누군가 = memberRepository.save(MemberFixture.createEthan()); - final Runner 러너_누군가 = runnerRepository.save(RunnerFixture.createRunner(사용자_누군가)); - 대기중인_게시글 = runnerPostRepository.save(RunnerPostFixture.create( - 러너_누군가, - null, - deadline(LocalDateTime.now().plusHours(100)), - ReviewStatus.NOT_STARTED - )); - supporterRunnerPostRepository.save(SupporterRunnerPostFixture.create(대기중인_게시글, 로그인된_서포터)); + @Test + void 로그인된_서포터는_서포터와_연관된_대기중인_러너_게시글_목록_조회에_성공한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); - 리뷰중인_게시글 = runnerPostRepository.save(RunnerPostFixture.create( - 러너_누군가, - 로그인된_서포터, - deadline(LocalDateTime.now().plusHours(100)), - ReviewStatus.IN_PROGRESS - )); - supporterRunnerPostRepository.save(SupporterRunnerPostFixture.create(리뷰중인_게시글, 로그인된_서포터)); + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); - 완료된_게시글 = runnerPostRepository.save(RunnerPostFixture.create( - 러너_누군가, - 로그인된_서포터, - deadline(LocalDateTime.now().plusHours(100)), - ReviewStatus.DONE - )); - supporterRunnerPostRepository.save(SupporterRunnerPostFixture.create(완료된_게시글, 로그인된_서포터)); - } + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); - @Test - void 로그인된_서포터의_대기중인_러너_게시글_목록_조회에_성공한다() { - final RunnerPostResponse.ReferencedBySupporter 서포터가_지원한_러너_게시글_응답 - = RunnerPostResponse.ReferencedBySupporter.of(대기중인_게시글, 1); - final PageResponse 페이징된_서포터가_지원한_러너_게시글_응답 - = 서포터가_리뷰_완료한_러너_게시글_응답(페이징_정보, List.of(서포터가_지원한_러너_게시글_응답)); + final RunnerPost 리뷰_신청을_대기중인_디투_러너_게시글 = runnerPostRepository.getByRunnerPostId(디투_러너_게시글_식별자값); + final Long 디투_러너_게시글에_지원한_서포터_수 = supporterRunnerPostRepository.getApplicantCountByRunnerPostId(디투_러너_게시글_식별자값); + + final RunnerPostResponse.ReferencedBySupporter 서포터가_리뷰를_지원한_대기중인_러너_게시글_응답 = 서포터와_연관된_러너_게시글_응답( + 리뷰_신청을_대기중인_디투_러너_게시글, + List.of("자바", "스프링"), + 0, + 디투_러너_게시글에_지원한_서포터_수, + ReviewStatus.NOT_STARTED + ); + // then RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(로그인용_토큰) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) .로그인한_서포터의_러너_게시글_페이징을_조회한다(ReviewStatus.NOT_STARTED, 페이징_정보) .서버_응답() - .서포터와_연관된_러너_게시글_페이징_조회_성공을_검증한다(페이징된_서포터가_지원한_러너_게시글_응답); + .서포터와_연관된_러너_게시글_페이징_조회_성공을_검증한다( + 서포터와_연관된_러너_게시글_페이징_응답(페이징_정보, List.of(서포터가_리뷰를_지원한_대기중인_러너_게시글_응답)) + ); } @Test void 로그인된_서포터의_진행중인_러너_게시글_목록_조회에_성공한다() { - final RunnerPostResponse.ReferencedBySupporter 서포터가_리뷰중인_러너_게시글_응답 - = RunnerPostResponse.ReferencedBySupporter.of(리뷰중인_게시글, 1); - final PageResponse 페이징된_서포터가_리뷰중인_러너_게시글_응답 - = 서포터가_리뷰_완료한_러너_게시글_응답(페이징_정보, List.of(서포터가_리뷰중인_러너_게시글_응답)); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Supporter 서포터_헤나 = supporterRepository.getBySocialId(헤나_소셜_아이디); + + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + 러너가_서포터의_리뷰_신청_선택에_성공한다(서포터_헤나, 디투_액세스_토큰, 디투_러너_게시글_식별자값); + + final RunnerPost 리뷰_진행중인_디투_러너_게시글 = runnerPostRepository.getByRunnerPostId(디투_러너_게시글_식별자값); + final Long 디투_러너_게시글에_지원한_서포터_수 = supporterRunnerPostRepository.getApplicantCountByRunnerPostId(디투_러너_게시글_식별자값); + + final RunnerPostResponse.ReferencedBySupporter 서포터가_리뷰를_진행중인_러너_게시글_응답 = 서포터와_연관된_러너_게시글_응답( + 리뷰_진행중인_디투_러너_게시글, + List.of("자바", "스프링"), + 0, + 디투_러너_게시글에_지원한_서포터_수, + ReviewStatus.IN_PROGRESS + ); + // then RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(로그인용_토큰) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) .로그인한_서포터의_러너_게시글_페이징을_조회한다(ReviewStatus.IN_PROGRESS, 페이징_정보) .서버_응답() - .서포터와_연관된_러너_게시글_페이징_조회_성공을_검증한다(페이징된_서포터가_리뷰중인_러너_게시글_응답); + .서포터와_연관된_러너_게시글_페이징_조회_성공을_검증한다( + 서포터와_연관된_러너_게시글_페이징_응답(페이징_정보, List.of(서포터가_리뷰를_진행중인_러너_게시글_응답)) + ); } @Test void 로그인된_서포터의_완료된_러너_게시글_목록_조회에_성공한다() { - final RunnerPostResponse.ReferencedBySupporter 서포터가_리뷰_완료한_러너_게시글_응답 - = RunnerPostResponse.ReferencedBySupporter.of(완료된_게시글, 1); - final PageResponse 페이징된_서포터가_리뷰_완료한_러너_게시글_응답 - = 서포터가_리뷰_완료한_러너_게시글_응답(페이징_정보, List.of(서포터가_리뷰_완료한_러너_게시글_응답)); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Supporter 서포터_헤나 = supporterRepository.getBySocialId(헤나_소셜_아이디); + + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + 러너가_서포터의_리뷰_신청_선택에_성공한다(서포터_헤나, 디투_액세스_토큰, 디투_러너_게시글_식별자값); + 서포터가_러너_게시글의_리뷰를_완료로_변경하는_것을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + + final RunnerPost 리뷰가_완료된_디투_러너_게시글 = runnerPostRepository.getByRunnerPostId(디투_러너_게시글_식별자값); + final Long 디투_러너_게시글에_지원한_서포터_수 = supporterRunnerPostRepository.getApplicantCountByRunnerPostId(디투_러너_게시글_식별자값); + + final RunnerPostResponse.ReferencedBySupporter 서포터가_리뷰를_완료한_러너_게시글_응답 = 서포터와_연관된_러너_게시글_응답( + 리뷰가_완료된_디투_러너_게시글, + List.of("자바", "스프링"), + 0, + 디투_러너_게시글에_지원한_서포터_수, + ReviewStatus.DONE + ); + // then RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(로그인용_토큰) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) .로그인한_서포터의_러너_게시글_페이징을_조회한다(ReviewStatus.DONE, 페이징_정보) .서버_응답() - .서포터와_연관된_러너_게시글_페이징_조회_성공을_검증한다(페이징된_서포터가_리뷰_완료한_러너_게시글_응답); + .서포터와_연관된_러너_게시글_페이징_조회_성공을_검증한다( + 서포터와_연관된_러너_게시글_페이징_응답(페이징_정보, List.of(서포터가_리뷰를_완료한_러너_게시글_응답)) + ); + } + + private Long 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(final String 헤나_액세스_토큰) { + return RunnerPostAssuredCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너가_러너_게시글을_작성한다( + 러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + List.of("자바", "스프링"), + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + ) + ) + + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); + } + + private void 서포터가_러너_게시글에_리뷰_신청을_성공한다(final String 헤나_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + RunnerPostAssuredCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .서포터가_러너_게시글에_리뷰를_신청한다(디투_러너_게시글_식별자값, "안녕하세요. 서포터 헤나입니다.") + + .서버_응답() + .서포터가_러너_게시글에_리뷰_신청_성공을_검증한다(디투_러너_게시글_식별자값); + } + + private void 러너가_서포터의_리뷰_신청_선택에_성공한다(final Supporter 서포터_헤나, final String 디투_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + RunnerPostAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(디투_액세스_토큰) + .러너가_서포터를_선택한다(디투_러너_게시글_식별자값, 러너의_서포터_선택_요청(서포터_헤나.getId())) + + .서버_응답() + .러너_게시글에_서포터가_성공적으로_선택되었는지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); + } + + private void 서포터가_러너_게시글의_리뷰를_완료로_변경하는_것을_성공한다(final String 헤나_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + RunnerPostAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .서포터가_리뷰를_완료하고_리뷰완료_버튼을_누른다(디투_러너_게시글_식별자값) + + .서버_응답() + .러너_게시글이_성공적으로_리뷰_완료_상태인지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); } } diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostReadWithSupporterIdAndReviewStatusAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostReadWithSupporterIdAndReviewStatusAssuredTest.java index 8381cd24d..63575e4f9 100644 --- a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostReadWithSupporterIdAndReviewStatusAssuredTest.java +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostReadWithSupporterIdAndReviewStatusAssuredTest.java @@ -2,96 +2,112 @@ import org.junit.jupiter.api.Test; import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpStatus; +import touch.baton.assure.common.HttpStatusAndLocationHeader; import touch.baton.config.AssuredTestConfig; -import touch.baton.domain.common.response.PageResponse; -import touch.baton.domain.member.Member; -import touch.baton.domain.runner.Runner; +import touch.baton.config.infra.auth.oauth.authcode.MockAuthCodes; +import touch.baton.domain.member.vo.SocialId; import touch.baton.domain.runnerpost.RunnerPost; import touch.baton.domain.runnerpost.controller.response.RunnerPostResponse; import touch.baton.domain.runnerpost.vo.ReviewStatus; import touch.baton.domain.supporter.Supporter; -import touch.baton.domain.supporter.SupporterRunnerPost; -import touch.baton.domain.technicaltag.TechnicalTag; -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 java.time.LocalDateTime; import java.util.List; -import static java.time.LocalDateTime.now; -import static touch.baton.assure.runnerpost.RunnerPostAssuredSupport.서포터가_리뷰_완료한_러너_게시글_응답; -import static touch.baton.fixture.domain.TechnicalTagFixture.createJava; -import static touch.baton.fixture.domain.TechnicalTagFixture.createSpring; -import static touch.baton.fixture.vo.DeadlineFixture.deadline; -import static touch.baton.fixture.vo.IntroductionFixture.introduction; -import static touch.baton.fixture.vo.MessageFixture.message; -import static touch.baton.fixture.vo.ReviewCountFixture.reviewCount; +import static touch.baton.assure.runnerpost.RunnerPostAssuredCreateSupport.러너_게시글_생성_요청; +import static touch.baton.assure.runnerpost.RunnerPostAssuredSupport.*; @SuppressWarnings("NonAsciiCharacters") class RunnerPostReadWithSupporterIdAndReviewStatusAssuredTest extends AssuredTestConfig { @Test void 서포터가_리뷰_완료한_러너_게시글_페이징_조회에_성공한다() { - final Runner 러너_에단 = 러너를_저장한다(MemberFixture.createEthan()); - final Supporter 서포터_헤나 = 서포터_헤나를_저장한다(); - final RunnerPost 러너_에단의_게시글 = 러너_게시글을_등록한다(러너_에단); - 러너_게시글에_서포터를_할당한다(서포터_헤나, 러너_에단의_게시글); - 서포터를_러너_게시글에_저장한다(서포터_헤나, 러너_에단의_게시글); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Supporter 서포터_헤나 = supporterRepository.getBySocialId(헤나_소셜_아이디); - // FIXME: 2023/08/13 러너 게시글 리뷰 완료 요청 기능 구현시 아래 ReviewStatus.DONE 테스트를 수정해야한다. - 러너_에단의_게시글.updateReviewStatus(ReviewStatus.DONE); - runnerPostRepository.save(러너_에단의_게시글); + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); - final String 헤나_액세스_토큰 = login(서포터_헤나.getMember().getSocialId().getValue()); + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + 러너가_서포터의_리뷰_신청_선택에_성공한다(서포터_헤나, 디투_액세스_토큰, 디투_러너_게시글_식별자값); + 서포터가_러너_게시글의_리뷰를_완료로_변경하는_것을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); final PageRequest 페이징_정보 = PageRequest.of(1, 10); - final RunnerPostResponse.ReferencedBySupporter 서포터가_리뷰_완료한_러너_게시글_응답 - = RunnerPostResponse.ReferencedBySupporter.of(러너_에단의_게시글, 1); - final PageResponse 페이징된_서포터가_리뷰_완료한_러너_게시글_응답 - = 서포터가_리뷰_완료한_러너_게시글_응답(페이징_정보, List.of(서포터가_리뷰_완료한_러너_게시글_응답)); - + final RunnerPost 리뷰_신청을_대기중인_디투_러너_게시글 = runnerPostRepository.getByRunnerPostId(디투_러너_게시글_식별자값); + final Long 디투_러너_게시글에_지원한_서포터_수 = supporterRunnerPostRepository.getApplicantCountByRunnerPostId(디투_러너_게시글_식별자값); + + final RunnerPostResponse.ReferencedBySupporter 서포터가_리뷰를_지원한_대기중인_러너_게시글_응답 = 서포터와_연관된_러너_게시글_응답( + 리뷰_신청을_대기중인_디투_러너_게시글, + List.of("자바", "스프링"), + 0, + 디투_러너_게시글에_지원한_서포터_수, + ReviewStatus.DONE + ); + + // then RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(헤나_액세스_토큰) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) .서포터와_연관된_러너_게시글_페이징을_조회한다(서포터_헤나.getId(), ReviewStatus.DONE, 페이징_정보) .서버_응답() - .서포터와_연관된_러너_게시글_페이징_조회_성공을_검증한다(페이징된_서포터가_리뷰_완료한_러너_게시글_응답); - } - - private RunnerPost 러너_게시글에_서포터를_할당한다(final Supporter 서포터_헤나, final RunnerPost 러너_에단의_게시글) { - 러너_에단의_게시글.assignSupporter(서포터_헤나); - return runnerPostRepository.save(러너_에단의_게시글); + .서포터와_연관된_러너_게시글_페이징_조회_성공을_검증한다( + 서포터와_연관된_러너_게시글_페이징_응답(페이징_정보, List.of(서포터가_리뷰를_지원한_대기중인_러너_게시글_응답)) + ); } - private RunnerPost 러너_게시글을_등록한다(final Runner 러너) { + private Long 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(final String 헤나_액세스_토큰) { + return RunnerPostAssuredCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너가_러너_게시글을_작성한다( + 러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + List.of("자바", "스프링"), + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + ) + ) - return runnerPostRepository.save(RunnerPostFixture.create(러너, deadline(now().plusHours(100)))); + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); } - private Runner 러너를_저장한다(final Member member) { - final Member 저장된_사용자 = memberRepository.save(member); + private void 서포터가_러너_게시글에_리뷰_신청을_성공한다(final String 헤나_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + RunnerPostAssuredCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .서포터가_러너_게시글에_리뷰를_신청한다(디투_러너_게시글_식별자값, "안녕하세요. 서포터 헤나입니다.") - return runnerRepository.save(RunnerFixture.createRunner(introduction("안녕하세요"), 저장된_사용자)); + .서버_응답() + .서포터가_러너_게시글에_리뷰_신청_성공을_검증한다(디투_러너_게시글_식별자값); } - private Supporter 서포터_헤나를_저장한다() { - final TechnicalTag 자바_기술_태그 = technicalTagRepository.save(createJava()); - final TechnicalTag 스프링_기술_태그 = technicalTagRepository.save(createSpring()); - final List 기술_태그_목록 = List.of(자바_기술_태그, 스프링_기술_태그); - final Member 저장된_사용자_헤나 = memberRepository.save(MemberFixture.createHyena()); + private void 러너가_서포터의_리뷰_신청_선택에_성공한다(final Supporter 서포터_헤나, final String 디투_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + RunnerPostAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(디투_액세스_토큰) + .러너가_서포터를_선택한다(디투_러너_게시글_식별자값, 러너의_서포터_선택_요청(서포터_헤나.getId())) - return supporterRepository.save(SupporterFixture.create(reviewCount(0), 저장된_사용자_헤나, 기술_태그_목록)); + .서버_응답() + .러너_게시글에_서포터가_성공적으로_선택되었는지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); } - private SupporterRunnerPost 서포터를_러너_게시글에_저장한다(final Supporter 서포터, final RunnerPost 러너_게시글) { - final SupporterRunnerPost 서포터_러너_게시글 = SupporterRunnerPost.builder() - .runnerPost(러너_게시글) - .supporter(서포터) - .message(message("안녕하세요. 서포터 헤나입니다.")) - .build(); + private void 서포터가_러너_게시글의_리뷰를_완료로_변경하는_것을_성공한다(final String 헤나_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + RunnerPostAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .서포터가_리뷰를_완료하고_리뷰완료_버튼을_누른다(디투_러너_게시글_식별자값) - return supporterRunnerPostRepository.save(서포터_러너_게시글); + .서버_응답() + .러너_게시글이_성공적으로_리뷰_완료_상태인지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); } } diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostUpdateAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostUpdateAssuredTest.java index bf475bbb4..af8400993 100644 --- a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostUpdateAssuredTest.java +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostUpdateAssuredTest.java @@ -4,76 +4,108 @@ 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.config.infra.auth.oauth.authcode.MockAuthCodes; +import touch.baton.domain.member.vo.SocialId; 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 java.util.List; -import static touch.baton.domain.runnerpost.vo.ReviewStatus.IN_PROGRESS; -import static touch.baton.fixture.vo.DeadlineFixture.deadline; +import static touch.baton.assure.runnerpost.RunnerPostAssuredCreateSupport.러너_게시글_생성_요청; +import static touch.baton.assure.runnerpost.RunnerPostAssuredSupport.러너의_서포터_선택_요청; @SuppressWarnings("NonAsciiCharacters") -public class RunnerPostUpdateAssuredTest extends AssuredTestConfig { +class RunnerPostUpdateAssuredTest extends AssuredTestConfig { @Test void 러너가_서포터_목록에서_서포터를_선택할_수_있다() { // given - final String 디투_소셜_아이디 = "hongSile"; - final Member 사용자_디투 = memberRepository.save(MemberFixture.createWithSocialId(디투_소셜_아이디)); - final Runner 러너_디투 = runnerRepository.save(RunnerFixture.createRunner(사용자_디투)); - final String 디투_토큰 = login(디투_소셜_아이디); + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Supporter 서포터_헤나 = supporterRepository.getBySocialId(헤나_소셜_아이디); - final RunnerPost 디투_게시글 = runnerPostRepository.save(RunnerPostFixture.create(러너_디투, deadline(LocalDateTime.now().plusDays(10)))); + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); - final Member 사용자_에단 = memberRepository.save(MemberFixture.createEthan()); - final Supporter 서포터_에단 = supporterRepository.save(SupporterFixture.create(사용자_에단)); + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); - 서포터가_리뷰_게시글에_리뷰_제안을_한다(디투_게시글, 서포터_에단); + // then + final RunnerPostUpdateRequest.SelectSupporter 러너의_서포터_선택_요청 = 러너의_서포터_선택_요청(서포터_헤나.getId()); - 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(지원할_게시글, 지원한_서포터)); - } - @Test void 서포터_리뷰완료_후_리뷰상태를_완료로_변경할_수_있다() { // given - final Member 사용자_에단 = memberRepository.save(MemberFixture.createEthan()); - final Runner 글_쓴_러너 = runnerRepository.save(RunnerFixture.createRunner(사용자_에단)); + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Supporter 서포터_헤나 = supporterRepository.getBySocialId(헤나_소셜_아이디); - final String 디투_소셜_아이디 = "hongSile"; - final Member 사용자_디투 = memberRepository.save(MemberFixture.createWithSocialId(디투_소셜_아이디)); - final Supporter 선택된_서포터 = supporterRepository.save(SupporterFixture.create(사용자_디투)); - final String 서포터_디투_토큰 = login(디투_소셜_아이디); + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); - final RunnerPost 서포터가_배정된_게시글 = runnerPostRepository.save(RunnerPostFixture.createWithReviewStatus(글_쓴_러너, 선택된_서포터, IN_PROGRESS)); + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + 러너가_서포터의_리뷰_신청_선택에_성공한다(서포터_헤나, 디투_액세스_토큰, 디투_러너_게시글_식별자값); // when, then RunnerPostAssuredSupport .클라이언트_요청() - .토큰으로_로그인한다(서포터_디투_토큰) - .서포터가_리뷰를_완료하고_리뷰완료_버튼을_누른다(서포터가_배정된_게시글.getId()) + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .서포터가_리뷰를_완료하고_리뷰완료_버튼을_누른다(디투_러너_게시글_식별자값) .서버_응답() .러너_게시글이_성공적으로_리뷰_완료_상태인지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); } + + + private Long 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(final String 헤나_액세스_토큰) { + return RunnerPostAssuredCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너가_러너_게시글을_작성한다( + 러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + List.of("자바", "스프링"), + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + ) + ) + + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); + } + + private void 서포터가_러너_게시글에_리뷰_신청을_성공한다(final String 헤나_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + RunnerPostAssuredCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .서포터가_러너_게시글에_리뷰를_신청한다(디투_러너_게시글_식별자값, "안녕하세요. 서포터 헤나입니다.") + + .서버_응답() + .서포터가_러너_게시글에_리뷰_신청_성공을_검증한다(디투_러너_게시글_식별자값); + } + + private void 러너가_서포터의_리뷰_신청_선택에_성공한다(final Supporter 서포터_헤나, final String 디투_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + RunnerPostAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(디투_액세스_토큰) + .러너가_서포터를_선택한다(디투_러너_게시글_식별자값, 러너의_서포터_선택_요청(서포터_헤나.getId())) + + .서버_응답() + .러너_게시글에_서포터가_성공적으로_선택되었는지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); + } } diff --git a/backend/baton/src/test/java/touch/baton/assure/supporter/SupporterAssuredSupport.java b/backend/baton/src/test/java/touch/baton/assure/supporter/SupporterAssuredSupport.java index 1b1721f54..1a27080bb 100644 --- a/backend/baton/src/test/java/touch/baton/assure/supporter/SupporterAssuredSupport.java +++ b/backend/baton/src/test/java/touch/baton/assure/supporter/SupporterAssuredSupport.java @@ -4,12 +4,16 @@ import io.restassured.response.Response; import org.springframework.http.HttpStatus; import touch.baton.assure.common.AssuredSupport; +import touch.baton.assure.common.PathParams; import touch.baton.domain.common.exception.ClientErrorCode; import touch.baton.domain.common.response.ErrorResponse; import touch.baton.domain.supporter.Supporter; import touch.baton.domain.supporter.controller.response.SupporterResponse; import touch.baton.domain.supporter.service.dto.SupporterUpdateRequest; +import java.util.List; +import java.util.Map; + import static org.assertj.core.api.SoftAssertions.assertSoftly; @SuppressWarnings("NonAsciiCharacters") @@ -22,8 +26,35 @@ private SupporterAssuredSupport() { return new SupporterClientRequestBuilder(); } - public static SupporterResponse.Profile 서포터_프로필_응답(final Supporter 서포터) { - return SupporterResponse.Profile.from(서포터); + public static SupporterUpdateRequest 서포터_본인_정보_수정_요청(final String 이름, + final String 회사, + final String 서포터_자기소개글, + final List 서포터_기술_태그_목록 + ) { + return new SupporterUpdateRequest(이름, 회사, 서포터_자기소개글, 서포터_기술_태그_목록); + } + + public static SupporterResponse.Profile 서포터_Profile_응답(final Supporter 서포터, final List 서포터_태그_목록) { + return new SupporterResponse.Profile( + 서포터.getId(), + 서포터.getMember().getMemberName().getValue(), + 서포터.getMember().getCompany().getValue(), + 서포터.getMember().getImageUrl().getValue(), + 서포터.getMember().getGithubUrl().getValue(), + 서포터.getIntroduction().getValue(), + 서포터_태그_목록 + ); + } + + public static SupporterResponse.MyProfile 서포터_MyProfile_응답(final Supporter 서포터, final List 서포터_태그_목록) { + return new SupporterResponse.MyProfile( + 서포터.getMember().getMemberName().getValue(), + 서포터.getMember().getImageUrl().getValue(), + 서포터.getMember().getGithubUrl().getValue(), + 서포터.getIntroduction().getValue(), + 서포터.getMember().getCompany().getValue(), + 서포터_태그_목록 + ); } public static class SupporterClientRequestBuilder { @@ -32,13 +63,13 @@ public static class SupporterClientRequestBuilder { private String accessToken; - public SupporterClientRequestBuilder 로그인_한다(final String 토큰) { - accessToken = 토큰; + public SupporterClientRequestBuilder 액세스_토큰으로_로그인_한다(final String 액세스_토큰) { + accessToken = 액세스_토큰; return this; } public SupporterClientRequestBuilder 서포터_프로필을_서포터_식별자값으로_조회한다(final Long 서포터_식별자값) { - response = AssuredSupport.get("/api/v1/profile/supporter/{supporterId}", "supporterId", 서포터_식별자값); + response = AssuredSupport.get("/api/v1/profile/supporter/{supporterId}", new PathParams(Map.of("supporterId", 서포터_식별자값))); return this; } @@ -47,7 +78,7 @@ public static class SupporterClientRequestBuilder { return this; } - public SupporterClientRequestBuilder 서포터_마이페이지를_토큰으로_조회한다() { + public SupporterClientRequestBuilder 서포터_마이페이지를_액세스_토큰으로_조회한다() { response = AssuredSupport.get("/api/v1/profile/supporter/me", accessToken); return this; } diff --git a/backend/baton/src/test/java/touch/baton/assure/supporter/SupporterReadBySupporterIdAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/supporter/SupporterReadBySupporterIdAssuredTest.java index 7b33fda8c..8d4ddad9a 100644 --- a/backend/baton/src/test/java/touch/baton/assure/supporter/SupporterReadBySupporterIdAssuredTest.java +++ b/backend/baton/src/test/java/touch/baton/assure/supporter/SupporterReadBySupporterIdAssuredTest.java @@ -2,71 +2,50 @@ import org.junit.jupiter.api.Test; import touch.baton.config.AssuredTestConfig; -import touch.baton.domain.member.Member; +import touch.baton.config.infra.auth.oauth.authcode.MockAuthCodes; +import touch.baton.domain.member.vo.SocialId; import touch.baton.domain.supporter.Supporter; -import touch.baton.domain.supporter.controller.response.SupporterResponse; -import touch.baton.domain.technicaltag.TechnicalTag; -import touch.baton.fixture.domain.MemberFixture; -import touch.baton.fixture.domain.SupporterFixture; -import touch.baton.fixture.domain.TechnicalTagFixture; -import java.util.List; +import java.util.Collections; -import static touch.baton.assure.supporter.SupporterAssuredSupport.서포터_프로필_응답; +import static touch.baton.assure.supporter.SupporterAssuredSupport.서포터_MyProfile_응답; +import static touch.baton.assure.supporter.SupporterAssuredSupport.서포터_Profile_응답; @SuppressWarnings("NonAsciiCharacters") class SupporterReadBySupporterIdAssuredTest extends AssuredTestConfig { @Test void 서포터_프로필을_조회한다() { - final Member 사용자_헤나 = memberRepository.save(MemberFixture.createHyena()); - final Supporter 서포터_헤나 = supporterRepository.save(SupporterFixture.create(사용자_헤나)); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Supporter 서포터_헤나 = supporterRepository.getBySocialId(헤나_소셜_아이디); + // when, then SupporterAssuredSupport .클라이언트_요청() .서포터_프로필을_서포터_식별자값으로_조회한다(서포터_헤나.getId()) .서버_응답() - .서포터_프로필_조회_성공을_검증한다(서포터_프로필_응답(서포터_헤나)); + .서포터_프로필_조회_성공을_검증한다(서포터_Profile_응답(서포터_헤나, Collections.emptyList())); } @Test void 서포터_마이페이지_프로필을_조회한다() { // given - final String 디투_소셜_아이디 = "hongsile"; - final Member 사용자_디투 = memberRepository.save(MemberFixture.createWithSocialId(디투_소셜_아이디)); - - final TechnicalTag 자바_태그 = technicalTagRepository.save(TechnicalTagFixture.createJava()); - final TechnicalTag 리액트_태그 = technicalTagRepository.save(TechnicalTagFixture.createReact()); + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); - final Supporter 서포터_디투 = supporterRepository.save(SupporterFixture.create(사용자_디투, List.of(자바_태그, 리액트_태그))); - final String 서포터_디투_토큰 = login(디투_소셜_아이디); + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Supporter 서포터_헤나 = supporterRepository.getBySocialId(헤나_소셜_아이디); // when, then SupporterAssuredSupport .클라이언트_요청() - .로그인_한다(서포터_디투_토큰) - .서포터_마이페이지를_토큰으로_조회한다() + .액세스_토큰으로_로그인_한다(헤나_액세스_토큰) + .서포터_마이페이지를_액세스_토큰으로_조회한다() .서버_응답() - .서포터_마이페이지_프로필_조회_성공을_검증한다(응답(서포터_디투)); - } - - private SupporterResponse.MyProfile 응답(final Supporter 서포터) { - final Member 사용자_디투 = 서포터.getMember(); - return new SupporterResponse.MyProfile( - 사용자_디투.getMemberName().getValue(), - 사용자_디투.getImageUrl().getValue(), - 사용자_디투.getGithubUrl().getValue(), - 서포터.getIntroduction().getValue(), - 사용자_디투.getCompany().getValue(), - 서포터_기술_스택(서포터) - ); - } - - private List 서포터_기술_스택(final Supporter 서포터) { - return 서포터.getSupporterTechnicalTags().getSupporterTechnicalTags().stream() - .map(supporterTechnicalTag -> supporterTechnicalTag.getTechnicalTag().getTagName().getValue()) - .toList(); + .서포터_마이페이지_프로필_조회_성공을_검증한다(서포터_MyProfile_응답(서포터_헤나, Collections.emptyList())); } } diff --git a/backend/baton/src/test/java/touch/baton/assure/supporter/SupporterRunnerPostAssuredSupport.java b/backend/baton/src/test/java/touch/baton/assure/supporter/SupporterRunnerPostAssuredSupport.java index bf607037e..2227f5e4f 100644 --- a/backend/baton/src/test/java/touch/baton/assure/supporter/SupporterRunnerPostAssuredSupport.java +++ b/backend/baton/src/test/java/touch/baton/assure/supporter/SupporterRunnerPostAssuredSupport.java @@ -2,10 +2,14 @@ import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; -import org.springframework.http.HttpStatus; import touch.baton.assure.common.AssuredSupport; +import touch.baton.assure.common.HttpStatusAndLocationHeader; +import touch.baton.assure.common.PathParams; + +import java.util.Map; import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.springframework.http.HttpHeaders.LOCATION; @SuppressWarnings("NonAsciiCharacters") public class SupporterRunnerPostAssuredSupport { @@ -23,13 +27,13 @@ public static class SupporterRunnerPostClientRequestBuilder { private String accessToken; - public SupporterRunnerPostClientRequestBuilder 로그인_한다(final String 토큰) { - accessToken = 토큰; + public SupporterRunnerPostClientRequestBuilder 액세스_토큰으로_로그인_한다(final String 액세스_토큰) { + accessToken = 액세스_토큰; return this; } public SupporterRunnerPostClientRequestBuilder 서포터가_리뷰_제안을_취소한다(final Long 러너_게시글_식별자값) { - response = AssuredSupport.patch("/api/v1/posts/runner/{runnerPostId}/cancelation", "runnerPostId", 러너_게시글_식별자값, accessToken); + response = AssuredSupport.patch("/api/v1/posts/runner/{runnerPostId}/cancelation", accessToken, new PathParams(Map.of("runnerPostId", 러너_게시글_식별자값))); return this; } @@ -46,13 +50,10 @@ public SupporterRunnerPostServerResponseBuilder(final ExtractableResponse { - softly.assertThat(response.statusCode()).isEqualTo(HTTP_STATUS.value()); - softly.assertThat(response.header(응답_헤더_이름)).isEqualTo(응답_헤더_값); + softly.assertThat(response.statusCode()).isEqualTo(응답상태_및_로케이션.getHttpStatus().value()); + softly.assertThat(response.header(LOCATION)).isEqualTo(응답상태_및_로케이션.getLocation()); }); } } diff --git a/backend/baton/src/test/java/touch/baton/assure/supporter/SupporterRunnerPostDeleteAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/supporter/SupporterRunnerPostDeleteAssuredTest.java index c300d5ae0..b1359bc0a 100644 --- a/backend/baton/src/test/java/touch/baton/assure/supporter/SupporterRunnerPostDeleteAssuredTest.java +++ b/backend/baton/src/test/java/touch/baton/assure/supporter/SupporterRunnerPostDeleteAssuredTest.java @@ -1,66 +1,68 @@ package touch.baton.assure.supporter; import org.junit.jupiter.api.Test; +import touch.baton.assure.common.HttpStatusAndLocationHeader; +import touch.baton.assure.runnerpost.RunnerPostAssuredCreateSupport; 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.vo.Deadline; -import touch.baton.domain.supporter.Supporter; -import touch.baton.domain.supporter.SupporterRunnerPost; -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 touch.baton.config.infra.auth.oauth.authcode.MockAuthCodes; import java.time.LocalDateTime; +import java.util.List; -import static org.springframework.http.HttpHeaders.LOCATION; import static org.springframework.http.HttpStatus.NO_CONTENT; +import static touch.baton.assure.runnerpost.RunnerPostAssuredCreateSupport.러너_게시글_생성_요청; @SuppressWarnings("NonAsciiCharacters") -public class SupporterRunnerPostDeleteAssuredTest extends AssuredTestConfig { +class SupporterRunnerPostDeleteAssuredTest extends AssuredTestConfig { @Test void 러너_게시글에_보낸_리뷰_제안을_취소한다() { - final String 로그인한_서포터의_소셜_id = "ditooSocialId"; - final Supporter 로그인한_서포터 = 로그인한_서포터를_저장한다(로그인한_서포터의_소셜_id); - final String 로그인한_서포터의_액세스_토큰 = login(로그인한_서포터의_소셜_id); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); - final Runner 리뷰_받고_싶은_러너 = 리뷰_받고_싶은_러너를_저장한다(); - final RunnerPost 리뷰_받을_게시글 = 리뷰_받을_게시글을_생성한다(리뷰_받고_싶은_러너); - 서포터가_러너_게시글에_리뷰_제안한다(로그인한_서포터, 리뷰_받을_게시글); - - final String 응답_헤더_이름 = LOCATION; - final String 응답_헤더_값 = "/api/v1/posts/runner/" + 리뷰_받을_게시글.getId(); + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + // then SupporterRunnerPostAssuredSupport .클라이언트_요청() - .로그인_한다(로그인한_서포터의_액세스_토큰) - .서포터가_리뷰_제안을_취소한다(리뷰_받을_게시글.getId()) + .액세스_토큰으로_로그인_한다(헤나_액세스_토큰) + .서포터가_리뷰_제안을_취소한다(디투_러너_게시글_식별자값) .서버_응답() - .서포터의_리뷰_제안_철회를_검증한다(NO_CONTENT, 응답_헤더_이름, 응답_헤더_값); + .서포터의_리뷰_제안_철회를_검증한다(new HttpStatusAndLocationHeader(NO_CONTENT, "/api/v1/posts/runner/" + 디투_러너_게시글_식별자값)); } - private Supporter 로그인한_서포터를_저장한다(final String 소셜_id) { - final Member 사용자 = memberRepository.save(MemberFixture.createWithSocialId(소셜_id)); - return supporterRepository.save(SupporterFixture.create(사용자)); - } + private Long 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(final String 헤나_액세스_토큰) { + return RunnerPostAssuredCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너가_러너_게시글을_작성한다( + 러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + List.of("자바", "스프링"), + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + ) + ) - private Runner 리뷰_받고_싶은_러너를_저장한다() { - final Member 사용자 = memberRepository.save(MemberFixture.createEthan()); - return runnerRepository.save(RunnerFixture.createRunner(사용자)); + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); } - private RunnerPost 리뷰_받을_게시글을_생성한다(final Runner 리뷰_받을_러너) { - final RunnerPost 러너_게시글 = RunnerPostFixture.create(리뷰_받을_러너, new Deadline(LocalDateTime.now().plusHours(100))); - return runnerPostRepository.save(러너_게시글); - } + private void 서포터가_러너_게시글에_리뷰_신청을_성공한다(final String 서포터_액세스_토큰, final Long 러너_게시글_식별자값) { + RunnerPostAssuredCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(서포터_액세스_토큰) + .서포터가_러너_게시글에_리뷰를_신청한다(러너_게시글_식별자값, "안녕하세요. 서포터 헤나입니다.") - private void 서포터가_러너_게시글에_리뷰_제안한다(final Supporter 서포터, final RunnerPost 러너_게시글) { - final SupporterRunnerPost 리뷰_제안 = SupporterRunnerPostFixture.create(러너_게시글, 서포터); - supporterRunnerPostRepository.save(리뷰_제안); + .서버_응답() + .서포터가_러너_게시글에_리뷰_신청_성공을_검증한다(러너_게시글_식별자값); } } diff --git a/backend/baton/src/test/java/touch/baton/assure/supporter/SupporterUpdateAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/supporter/SupporterUpdateAssuredTest.java index 87f0448b7..0799a8cb9 100644 --- a/backend/baton/src/test/java/touch/baton/assure/supporter/SupporterUpdateAssuredTest.java +++ b/backend/baton/src/test/java/touch/baton/assure/supporter/SupporterUpdateAssuredTest.java @@ -1,43 +1,33 @@ package touch.baton.assure.supporter; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import touch.baton.config.AssuredTestConfig; -import touch.baton.domain.member.Member; -import touch.baton.domain.supporter.Supporter; -import touch.baton.domain.supporter.service.dto.SupporterUpdateRequest; -import touch.baton.fixture.domain.MemberFixture; -import touch.baton.fixture.domain.SupporterFixture; +import touch.baton.config.infra.auth.oauth.authcode.MockAuthCodes; import java.util.List; import static org.springframework.http.HttpStatus.NO_CONTENT; +import static touch.baton.assure.supporter.SupporterAssuredSupport.서포터_본인_정보_수정_요청; import static touch.baton.domain.common.exception.ClientErrorCode.COMPANY_IS_NULL; import static touch.baton.domain.common.exception.ClientErrorCode.NAME_IS_NULL; import static touch.baton.domain.common.exception.ClientErrorCode.SUPPORTER_INTRODUCTION_IS_NULL; import static touch.baton.domain.common.exception.ClientErrorCode.SUPPORTER_TECHNICAL_TAGS_ARE_NULL; @SuppressWarnings("NonAsciiCharacters") -public class SupporterUpdateAssuredTest extends AssuredTestConfig { - - private String 디투_액세스_토큰; - - @BeforeEach - void setUp() { - final String 디투_소셜_id = "ditooSocialId"; - final Member 사용자_디투 = memberRepository.save(MemberFixture.createWithSocialId(디투_소셜_id)); - final Supporter 서포터_디투 = supporterRepository.save(SupporterFixture.create(사용자_디투)); - 디투_액세스_토큰 = login(디투_소셜_id); - } +class SupporterUpdateAssuredTest extends AssuredTestConfig { @Test void 서포터_정보를_수정한다() { - final SupporterUpdateRequest 서포터_수정_요청_값 = new SupporterUpdateRequest("디투랜드", "우아한테크코스", "안녕하세요.", List.of("java", "spring")); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + // when, then SupporterAssuredSupport .클라이언트_요청() - .로그인_한다(디투_액세스_토큰) - .서포터_본인_프로필을_수정한다(서포터_수정_요청_값) + .액세스_토큰으로_로그인_한다(헤나_액세스_토큰) + .서포터_본인_프로필을_수정한다( + 서포터_본인_정보_수정_요청("수정된_이름", "수정된_회사", "수정된_서포터_자기소개글", List.of("자바", "스프링")) + ) .서버_응답() .서포터_본인_프로필_수정_성공을_검증한다(NO_CONTENT); @@ -45,12 +35,16 @@ void setUp() { @Test void 서포터_정보_수정_시에_이름이_없으면_예외가_발생한다() { - final SupporterUpdateRequest 서포터_수정_요청_값 = new SupporterUpdateRequest(null, "우아한테크코스", "안녕하세요.", List.of("java", "spring")); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + // when, then SupporterAssuredSupport .클라이언트_요청() - .로그인_한다(디투_액세스_토큰) - .서포터_본인_프로필을_수정한다(서포터_수정_요청_값) + .액세스_토큰으로_로그인_한다(헤나_액세스_토큰) + .서포터_본인_프로필을_수정한다( + 서포터_본인_정보_수정_요청(null, "수정된_회사", "수정된_서포터_자기소개글", List.of("자바", "스프링")) + ) .서버_응답() .서포터_본인_프로필_수정_실패를_검증한다(NAME_IS_NULL); @@ -58,12 +52,16 @@ void setUp() { @Test void 서포터_정보_수정_시에_소속이_없으면_예외가_발생한다() { - final SupporterUpdateRequest 서포터_수정_요청_값 = new SupporterUpdateRequest("디투랜드", null, "안녕하세요.", List.of("java", "spring")); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + // when, then SupporterAssuredSupport .클라이언트_요청() - .로그인_한다(디투_액세스_토큰) - .서포터_본인_프로필을_수정한다(서포터_수정_요청_값) + .액세스_토큰으로_로그인_한다(헤나_액세스_토큰) + .서포터_본인_프로필을_수정한다( + 서포터_본인_정보_수정_요청("수정된_이름", null, "수정된_서포터_자기소개글", List.of("자바", "스프링")) + ) .서버_응답() .서포터_본인_프로필_수정_실패를_검증한다(COMPANY_IS_NULL); @@ -71,12 +69,16 @@ void setUp() { @Test void 서포터_정보_수정_시에_소개글이_없으면_예외가_발생한다() { - final SupporterUpdateRequest 서포터_수정_요청_값 = new SupporterUpdateRequest("디투랜드", "배달의민족", null, List.of("java", "spring")); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + // when, then SupporterAssuredSupport .클라이언트_요청() - .로그인_한다(디투_액세스_토큰) - .서포터_본인_프로필을_수정한다(서포터_수정_요청_값) + .액세스_토큰으로_로그인_한다(헤나_액세스_토큰) + .서포터_본인_프로필을_수정한다( + 서포터_본인_정보_수정_요청("수정된_이름", "수정된_회사", null, List.of("자바", "스프링")) + ) .서버_응답() .서포터_본인_프로필_수정_실패를_검증한다(SUPPORTER_INTRODUCTION_IS_NULL); @@ -84,12 +86,16 @@ void setUp() { @Test void 서포터_정보_수정_시에_기술_태그가_없으면_예외가_발생한다() { - final SupporterUpdateRequest 서포터_수정_요청_값 = new SupporterUpdateRequest("디투랜드", "배달의민족", "배달왕이 될거에요.", null); + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + // when, then SupporterAssuredSupport .클라이언트_요청() - .로그인_한다(디투_액세스_토큰) - .서포터_본인_프로필을_수정한다(서포터_수정_요청_값) + .액세스_토큰으로_로그인_한다(헤나_액세스_토큰) + .서포터_본인_프로필을_수정한다( + 서포터_본인_정보_수정_요청("수정된_이름", "수정된_회사", "수정된_서포터_자기소개글", null) + ) .서버_응답() .서포터_본인_프로필_수정_실패를_검증한다(SUPPORTER_TECHNICAL_TAGS_ARE_NULL); diff --git a/backend/baton/src/test/java/touch/baton/assure/tag/TagAssuredSupport.java b/backend/baton/src/test/java/touch/baton/assure/tag/TagAssuredSupport.java new file mode 100644 index 000000000..d95fef753 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/tag/TagAssuredSupport.java @@ -0,0 +1,85 @@ +package touch.baton.assure.tag; + +import io.restassured.common.mapper.TypeRef; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.springframework.http.HttpStatus; +import touch.baton.assure.common.AssuredSupport; +import touch.baton.assure.common.QueryParams; +import touch.baton.domain.runnerpost.service.dto.RunnerPostCreateRequest; +import touch.baton.domain.tag.Tag; +import touch.baton.domain.tag.controller.response.TagSearchResponse; +import touch.baton.domain.tag.controller.response.TagSearchResponses; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +class TagAssuredSupport { + + private TagAssuredSupport() { + } + + public static TagAssuredSupport.TagClientRequestBuilder 클라이언트_요청() { + return new TagAssuredSupport.TagClientRequestBuilder(); + } + + public static class TagClientRequestBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public TagClientRequestBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; + return this; + } + + public TagClientRequestBuilder 러너_게시글_등록_요청한다(final RunnerPostCreateRequest 게시글_생성_요청) { + response = AssuredSupport.post("/api/v1/posts/runner", accessToken, 게시글_생성_요청); + return this; + } + + public TagClientRequestBuilder 태그_이름을_오름차순으로_10개_검색한다(final String 태그_이름) { + response = AssuredSupport.get("/api/v1/tags/search", new QueryParams(Map.of("tagName", 태그_이름))); + return this; + } + + public TagServerResponseBuilder 서버_응답() { + return new TagServerResponseBuilder(response); + } + + } + + public static class TagServerResponseBuilder { + + private final ExtractableResponse response; + + public TagServerResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + + public void 태그_검색_성공을_검증한다(final TagSearchResponses.Detail 검색된_태그_목록) { + final TagSearchResponses.Detail actual = this.response.as(new TypeRef<>() { + + }); + + assertSoftly(softly -> { + softly.assertThat(this.response.statusCode()).isEqualTo(HttpStatus.OK.value()); + softly.assertThat(actual.data()).isEqualTo(검색된_태그_목록.data()); + } + ); + } + } + + public static TagSearchResponses.Detail 태그_검색_Detail_응답(final List 검색된_태그_목록) { + List 태그_목록_응답 = 검색된_태그_목록.stream() + .map(tag -> TagSearchResponse.TagResponse.from(tag)) + .toList(); + final TagSearchResponses.Detail 검색된_태그_목록_응답들 = TagSearchResponses.Detail.from(태그_목록_응답); + + return 검색된_태그_목록_응답들; + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/tag/TagReadAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/tag/TagReadAssuredTest.java new file mode 100644 index 000000000..812c068d5 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/tag/TagReadAssuredTest.java @@ -0,0 +1,58 @@ +package touch.baton.assure.tag; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.runnerpost.RunnerPostAssuredCreateSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.MockAuthCodes; +import touch.baton.domain.tag.Tag; +import touch.baton.domain.tag.vo.TagReducedName; + +import java.time.LocalDateTime; +import java.util.List; + +import static touch.baton.assure.runnerpost.RunnerPostAssuredCreateSupport.러너_게시글_생성_요청; +import static touch.baton.assure.tag.TagAssuredSupport.태그_검색_Detail_응답; + +@SuppressWarnings("NonAsciiCharacters") +class TagReadAssuredTest extends AssuredTestConfig { + + @Test + void 태그_검색에_성공한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(MockAuthCodes.hyenaAuthCode()); + 러너_게시글_생성을_성공한다(헤나_액세스_토큰, List.of("java", "javascript", "script")); + final TagReducedName reducedName = TagReducedName.from("ja"); + final List 검색된_태그_목록 = tagRepository.readTagsByReducedName(reducedName); + + // when, then + TagAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .태그_이름을_오름차순으로_10개_검색한다("ja") + + .서버_응답() + .태그_검색_성공을_검증한다( + 태그_검색_Detail_응답(검색된_태그_목록) + ); + } + + public static void 러너_게시글_생성을_성공한다(final String 사용자_액세스_토큰, final List 태그_목록) { + RunnerPostAssuredCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(사용자_액세스_토큰) + .러너가_러너_게시글을_작성한다( + 러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + 태그_목록, + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + ) + ) + + .서버_응답() + .러너_게시글_생성_성공을_검증한다(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/common/schedule/ScheduleRunnerPostRepositoryTest.java b/backend/baton/src/test/java/touch/baton/common/schedule/ScheduleRunnerPostRepositoryTest.java index 130730e44..912f5f77d 100644 --- a/backend/baton/src/test/java/touch/baton/common/schedule/ScheduleRunnerPostRepositoryTest.java +++ b/backend/baton/src/test/java/touch/baton/common/schedule/ScheduleRunnerPostRepositoryTest.java @@ -10,6 +10,7 @@ import touch.baton.domain.runner.Runner; import touch.baton.domain.runnerpost.RunnerPost; import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.IsReviewed; import touch.baton.domain.runnerpost.vo.ReviewStatus; import touch.baton.fixture.domain.MemberFixture; import touch.baton.fixture.domain.RunnerFixture; @@ -19,7 +20,9 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; -import static touch.baton.domain.runnerpost.vo.ReviewStatus.*; +import static touch.baton.domain.runnerpost.vo.ReviewStatus.DONE; +import static touch.baton.domain.runnerpost.vo.ReviewStatus.NOT_STARTED; +import static touch.baton.domain.runnerpost.vo.ReviewStatus.OVERDUE; import static touch.baton.fixture.vo.DeadlineFixture.deadline; class ScheduleRunnerPostRepositoryTest extends RepositoryTestConfig { @@ -68,8 +71,8 @@ void updateAllPassedDeadline_fail_when_reviewStatus_is_DONE() { final Deadline passedDeadlineOne = deadline(LocalDateTime.now().minusHours(10)); final Deadline passedDeadlineTwo = deadline(LocalDateTime.now().minusHours(5)); final ReviewStatus expectedReviewStatus = DONE; - final RunnerPost runnerPostOne = RunnerPostFixture.create(runner, passedDeadlineOne, expectedReviewStatus); - final RunnerPost runnerPostTwo = RunnerPostFixture.create(runner, passedDeadlineTwo, expectedReviewStatus); + final RunnerPost runnerPostOne = RunnerPostFixture.create(runner, passedDeadlineOne, expectedReviewStatus, IsReviewed.notReviewed()); + final RunnerPost runnerPostTwo = RunnerPostFixture.create(runner, passedDeadlineTwo, expectedReviewStatus, IsReviewed.notReviewed()); em.persist(runnerPostOne); em.persist(runnerPostTwo); @@ -90,8 +93,8 @@ void updateAllPassedDeadline_fail_when_deadline_is_not_passed() { final Deadline passedDeadlineOne = deadline(LocalDateTime.now().plusHours(10)); final Deadline passedDeadlineTwo = deadline(LocalDateTime.now().plusHours(5)); final ReviewStatus expectedReviewStatus = NOT_STARTED; - final RunnerPost runnerPostOne = RunnerPostFixture.create(runner, passedDeadlineOne, expectedReviewStatus); - final RunnerPost runnerPostTwo = RunnerPostFixture.create(runner, passedDeadlineTwo, expectedReviewStatus); + final RunnerPost runnerPostOne = RunnerPostFixture.create(runner, passedDeadlineOne, expectedReviewStatus, IsReviewed.notReviewed()); + final RunnerPost runnerPostTwo = RunnerPostFixture.create(runner, passedDeadlineTwo, expectedReviewStatus, IsReviewed.notReviewed()); em.persist(runnerPostOne); em.persist(runnerPostTwo); 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 3bd2e8b24..5f13b743b 100644 --- a/backend/baton/src/test/java/touch/baton/config/AssuredTestConfig.java +++ b/backend/baton/src/test/java/touch/baton/config/AssuredTestConfig.java @@ -1,69 +1,68 @@ package touch.baton.config; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; import io.restassured.RestAssured; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestExecutionListeners; +import touch.baton.assure.common.JwtTestManager; +import touch.baton.assure.common.OauthLoginTestManager; +import touch.baton.assure.repository.TestMemberRepository; +import touch.baton.assure.repository.TestRefreshTokenRepository; +import touch.baton.assure.repository.TestRunnerPostReadRepository; +import touch.baton.assure.repository.TestRunnerPostRepository; +import touch.baton.assure.repository.TestRunnerRepository; +import touch.baton.assure.repository.TestSupporterRepository; +import touch.baton.assure.repository.TestSupporterRunnerPostRepository; +import touch.baton.assure.repository.TestTagRepository; +import touch.baton.assure.repository.TestTechnicalTagRepository; import touch.baton.config.converter.ConverterConfig; -import touch.baton.domain.member.repository.MemberRepository; -import touch.baton.domain.runner.repository.RunnerRepository; -import touch.baton.domain.runnerpost.repository.RunnerPostRepository; -import touch.baton.domain.supporter.repository.SupporterRepository; -import touch.baton.domain.supporter.repository.SupporterRunnerPostRepository; -import touch.baton.domain.technicaltag.repository.TechnicalTagRepository; -import touch.baton.infra.auth.jwt.JwtDecoder; +import touch.baton.config.infra.auth.MockAuthTestConfig; +import touch.baton.config.infra.github.MockGithubBranchServiceConfig; -import java.util.UUID; - -import static org.mockito.BDDMockito.when; - -@Import({JpaConfig.class, ConverterConfig.class, PageableTestConfig.class}) +@ActiveProfiles("test") +@Import({JpaConfig.class, ConverterConfig.class, PageableTestConfig.class, MockAuthTestConfig.class, MockGithubBranchServiceConfig.class, JwtTestManager.class}) @TestExecutionListeners(value = AssuredTestExecutionListener.class, mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS) @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public abstract class AssuredTestConfig { @Autowired - protected MemberRepository memberRepository; + protected TestMemberRepository memberRepository; @Autowired - protected RunnerRepository runnerRepository; + protected TestRunnerRepository runnerRepository; @Autowired - protected SupporterRepository supporterRepository; + protected TestSupporterRepository supporterRepository; @Autowired - protected RunnerPostRepository runnerPostRepository; + protected TestRunnerPostRepository runnerPostRepository; @Autowired - protected SupporterRunnerPostRepository supporterRunnerPostRepository; + protected TestRunnerPostReadRepository runnerPostReadRepository; @Autowired - protected TechnicalTagRepository technicalTagRepository; + protected TestSupporterRunnerPostRepository supporterRunnerPostRepository; - @MockBean - private JwtDecoder jwtDecoder; + @Autowired + protected TestTagRepository tagRepository; - @BeforeEach - void assuredTestSetUp(@LocalServerPort int port) { - RestAssured.port = port; - } + @Autowired + protected TestRefreshTokenRepository refreshTokenRepository; - public String login(final String socialId) { - final String token = UUID.randomUUID().toString(); - final Claims claims = Jwts.claims(); - claims.put("socialId", socialId); + @Autowired + protected JwtTestManager jwtTestManager; - when(jwtDecoder.parseJwtToken(token)).thenReturn(claims); + protected OauthLoginTestManager oauthLoginTestManager = new OauthLoginTestManager(); - return token; + @BeforeEach + void assuredTestSetUp(@LocalServerPort final int port) { + RestAssured.port = port; } } diff --git a/backend/baton/src/test/java/touch/baton/config/RestdocsConfig.java b/backend/baton/src/test/java/touch/baton/config/RestdocsConfig.java index 251c8d122..0b2faed47 100644 --- a/backend/baton/src/test/java/touch/baton/config/RestdocsConfig.java +++ b/backend/baton/src/test/java/touch/baton/config/RestdocsConfig.java @@ -103,7 +103,7 @@ protected String getAccessTokenBySocialId(final String socialId) { final Claims claims = Jwts.claims(); claims.put("socialId", socialId); - when(jwtDecoder.parseJwtToken(any())).thenReturn(claims); + when(jwtDecoder.parseAuthorizationHeader(any())).thenReturn(claims); return token; } diff --git a/backend/baton/src/test/java/touch/baton/config/ServiceTestConfig.java b/backend/baton/src/test/java/touch/baton/config/ServiceTestConfig.java index 6d123dd5e..e9aab1534 100644 --- a/backend/baton/src/test/java/touch/baton/config/ServiceTestConfig.java +++ b/backend/baton/src/test/java/touch/baton/config/ServiceTestConfig.java @@ -4,6 +4,7 @@ import touch.baton.domain.feedback.repository.SupporterFeedbackRepository; import touch.baton.domain.member.repository.MemberRepository; import touch.baton.domain.runner.repository.RunnerRepository; +import touch.baton.domain.runnerpost.repository.RunnerPostReadRepository; import touch.baton.domain.runnerpost.repository.RunnerPostRepository; import touch.baton.domain.supporter.repository.SupporterRepository; import touch.baton.domain.supporter.repository.SupporterRunnerPostRepository; @@ -27,6 +28,9 @@ public abstract class ServiceTestConfig extends RepositoryTestConfig { @Autowired protected RunnerPostRepository runnerPostRepository; + @Autowired + protected RunnerPostReadRepository runnerPostReadRepository; + @Autowired protected SupporterRunnerPostRepository supporterRunnerPostRepository; diff --git a/backend/baton/src/test/java/touch/baton/config/infra/auth/MockAuthTestConfig.java b/backend/baton/src/test/java/touch/baton/config/infra/auth/MockAuthTestConfig.java new file mode 100644 index 000000000..f9a368d62 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/config/infra/auth/MockAuthTestConfig.java @@ -0,0 +1,16 @@ +package touch.baton.config.infra.auth; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; +import touch.baton.config.infra.auth.jwt.MockJwtConfig; +import touch.baton.config.infra.auth.oauth.authcode.MockAuthCodeRequestUrlProviderCompositeConfig; +import touch.baton.config.infra.auth.oauth.client.MockOauthInformationClientCompositeConfig; + +@Profile("test") +@Import({MockJwtConfig.class, + MockAuthCodeRequestUrlProviderCompositeConfig.class, + MockOauthInformationClientCompositeConfig.class}) +@TestConfiguration +public abstract class MockAuthTestConfig { +} diff --git a/backend/baton/src/test/java/touch/baton/config/infra/auth/jwt/MockJwtConfig.java b/backend/baton/src/test/java/touch/baton/config/infra/auth/jwt/MockJwtConfig.java new file mode 100644 index 000000000..44eb2e03d --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/config/infra/auth/jwt/MockJwtConfig.java @@ -0,0 +1,34 @@ +package touch.baton.config.infra.auth.jwt; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import touch.baton.infra.auth.jwt.JwtConfig; +import touch.baton.infra.auth.jwt.JwtDecoder; +import touch.baton.infra.auth.jwt.JwtEncoder; + +@TestConfiguration +public abstract class MockJwtConfig { + + @Bean + JwtEncoder jwtEncoder() { + return new JwtEncoder(mockJwtConfig()); + } + + @Bean + JwtDecoder jwtDecoder() { + return new JwtDecoder(mockJwtConfig()); + } + + private JwtConfig mockJwtConfig() { + return new JwtConfig("test_secret_key_test_secret_key_test_secret_key_test_secret_key_test_secret_key_test_secret_key_test_secret_key_test_secret_key", "test_issuer", 30); + } + + @Bean + JwtEncoder jwtExpireEncoder() { + return new JwtEncoder(mockJwtExpireConfig()); + } + + private JwtConfig mockJwtExpireConfig() { + return new JwtConfig("test_secret_key_test_secret_key_test_secret_key_test_secret_key_test_secret_key_test_secret_key_test_secret_key_test_secret_key", "test_issuer", -1); + } +} diff --git a/backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/MockRefreshTokenConfig.java b/backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/MockRefreshTokenConfig.java new file mode 100644 index 000000000..86453ceac --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/MockRefreshTokenConfig.java @@ -0,0 +1,35 @@ +package touch.baton.config.infra.auth.oauth; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import touch.baton.domain.oauth.token.RefreshToken; +import touch.baton.infra.auth.jwt.JwtConfig; +import touch.baton.infra.auth.jwt.JwtEncoder; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@TestConfiguration +public class MockRefreshTokenConfig { + + @Bean + JwtEncoder expiredJwtDecoder() { + return new JwtEncoder(mockJwtConfig()); + } + + private JwtConfig mockJwtConfig() { + return new JwtConfig("test_secret_key_test_secret_key_test_secret_key_test_secret_key_test_secret_key_test_secret_key_test_secret_key_test_secret_key", "test_issuer", -1); + } + + @Bean + RefreshToken refreshToken() { + final RefreshToken mock = mock(RefreshToken.class); + + /** + * 다야한 when 절 + */ + when(mock.getToken().getValue()).thenReturn("mock refresh token"); + + return mock; + } +} diff --git a/backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/authcode/MockAuthCodeRequestUrlProviderCompositeConfig.java b/backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/authcode/MockAuthCodeRequestUrlProviderCompositeConfig.java new file mode 100644 index 000000000..25a5c7bd5 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/authcode/MockAuthCodeRequestUrlProviderCompositeConfig.java @@ -0,0 +1,22 @@ +package touch.baton.config.infra.auth.oauth.authcode; + +import org.mockito.Mockito; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import touch.baton.domain.oauth.OauthType; +import touch.baton.domain.oauth.authcode.AuthCodeRequestUrlProviderComposite; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.when; + +@TestConfiguration +public abstract class MockAuthCodeRequestUrlProviderCompositeConfig { + + @Bean + AuthCodeRequestUrlProviderComposite authCodeRequestUrlProviderComposite() { + final AuthCodeRequestUrlProviderComposite mock = Mockito.mock(AuthCodeRequestUrlProviderComposite.class); + when(mock.findRequestUrl(any(OauthType.class))).thenReturn("https://redirect-test.com"); + + return mock; + } +} diff --git a/backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/authcode/MockAuthCodes.java b/backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/authcode/MockAuthCodes.java new file mode 100644 index 000000000..8f4d51463 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/authcode/MockAuthCodes.java @@ -0,0 +1,20 @@ +package touch.baton.config.infra.auth.oauth.authcode; + +public abstract class MockAuthCodes { + + public static String ditooAuthCode() { + return "ditoo_auth_code"; + } + + public static String ethanAuthCode() { + return "ethan_auth_code"; + } + + public static String hyenaAuthCode() { + return "hyena_auth_code"; + } + + public static String judyAuthCode() { + return "judy_auth_code"; + } +} diff --git a/backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/client/MockOauthInformationClientCompositeConfig.java b/backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/client/MockOauthInformationClientCompositeConfig.java new file mode 100644 index 000000000..fbd78888f --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/client/MockOauthInformationClientCompositeConfig.java @@ -0,0 +1,52 @@ +package touch.baton.config.infra.auth.oauth.client; + +import org.mockito.Mockito; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import touch.baton.config.infra.auth.oauth.authcode.MockAuthCodes; +import touch.baton.domain.member.Member; +import touch.baton.domain.oauth.OauthInformation; +import touch.baton.domain.oauth.OauthType; +import touch.baton.domain.oauth.client.OauthInformationClientComposite; +import touch.baton.domain.oauth.token.SocialToken; +import touch.baton.fixture.domain.MemberFixture; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.when; + +@TestConfiguration +public abstract class MockOauthInformationClientCompositeConfig { + + @Bean + OauthInformationClientComposite oauthInformationClientComposite() { + final OauthInformationClientComposite mock = Mockito.mock(OauthInformationClientComposite.class); + + when(mock.fetchInformation(any(OauthType.class), eq(MockAuthCodes.ditooAuthCode()))) + .thenReturn(oauthInformation(MemberFixture.createDitoo(), "ditoo_access_token")); + + when(mock.fetchInformation(any(OauthType.class), eq(MockAuthCodes.ethanAuthCode()))) + .thenReturn(oauthInformation(MemberFixture.createEthan(), "ethan_access_token")); + + when(mock.fetchInformation(any(OauthType.class), eq(MockAuthCodes.hyenaAuthCode()))) + .thenReturn(oauthInformation(MemberFixture.createHyena(), "hyena_access_token")); + + when(mock.fetchInformation(any(OauthType.class), eq(MockAuthCodes.judyAuthCode()))) + .thenReturn(oauthInformation(MemberFixture.createJudy(), "judy_access_token")); + + return mock; + } + + public OauthInformation oauthInformation(final Member member, final String mockSocialToken) { + + return OauthInformation.builder() + .oauthId(member.getOauthId()) + .socialToken(new SocialToken(mockSocialToken)) + .oauthId(member.getOauthId()) + .memberName(member.getMemberName()) + .socialId(member.getSocialId()) + .githubUrl(member.getGithubUrl()) + .imageUrl(member.getImageUrl()) + .build(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/config/infra/github/MockGithubBranchServiceConfig.java b/backend/baton/src/test/java/touch/baton/config/infra/github/MockGithubBranchServiceConfig.java new file mode 100644 index 000000000..fe0a0bf26 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/config/infra/github/MockGithubBranchServiceConfig.java @@ -0,0 +1,21 @@ +package touch.baton.config.infra.github; + +import org.mockito.Mockito; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import touch.baton.infra.github.GithubBranchManager; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; + +@TestConfiguration +public abstract class MockGithubBranchServiceConfig { + + @Bean + public GithubBranchManager githubBranchService() { + final GithubBranchManager mock = Mockito.mock(GithubBranchManager.class); + doNothing().when(mock).createBranch(any(String.class), any(String.class)); + + return mock; + } +} diff --git a/backend/baton/src/test/java/touch/baton/document/github/GithubBranchApiTest.java b/backend/baton/src/test/java/touch/baton/document/github/GithubBranchApiTest.java new file mode 100644 index 000000000..339165dfd --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/document/github/GithubBranchApiTest.java @@ -0,0 +1,65 @@ +package touch.baton.document.github; + +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.member.controller.MemberBranchController; +import touch.baton.domain.member.service.dto.GithubBranchManageable; +import touch.baton.domain.member.service.dto.GithubRepoNameRequest; +import touch.baton.fixture.domain.MemberFixture; + +import java.util.Optional; + +import static org.apache.http.HttpHeaders.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.restdocs.headers.HeaderDocumentation.*; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(MemberBranchController.class) +class GithubBranchApiTest extends RestdocsConfig { + + @MockBean + private GithubBranchManageable githubBranchManageable; + + @BeforeEach + void setup() { + restdocsSetUp(new MemberBranchController(githubBranchManageable)); + } + + @DisplayName("깃허브 레포 브랜치 생성 API") + @Test + void createMemberBranch() throws Exception { + // given + final String socialId = "hongSile"; + final Member member = MemberFixture.createWithSocialId(socialId); + final String accessToken = getAccessTokenBySocialId(socialId); + final GithubRepoNameRequest request = new GithubRepoNameRequest("drunken-ditoo"); + + // when + when(oauthMemberRepository.findBySocialId(any())).thenReturn(Optional.ofNullable(member)); + doNothing().when(githubBranchManageable).createBranch(eq(socialId), anyString()); + + // then + mockMvc.perform(post("/api/v1/branch") + .header(AUTHORIZATION, "Bearer " + accessToken) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(redirectedUrl("/api/v1/profile/me")) + .andDo(restDocs.document( + requestHeaders(headerWithName(AUTHORIZATION).description("Bearer Token"), + headerWithName(CONTENT_TYPE).description(APPLICATION_JSON_VALUE)), + responseHeaders(headerWithName(LOCATION).description("Redirect URI")) + )); + } +} diff --git a/backend/baton/src/test/java/touch/baton/document/oauth/github/GithubOauthApiTest.java b/backend/baton/src/test/java/touch/baton/document/oauth/github/GithubOauthApiTest.java index 3681e2af1..eddd94c76 100644 --- a/backend/baton/src/test/java/touch/baton/document/oauth/github/GithubOauthApiTest.java +++ b/backend/baton/src/test/java/touch/baton/document/oauth/github/GithubOauthApiTest.java @@ -1,6 +1,7 @@ package touch.baton.document.oauth.github; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -9,13 +10,25 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.TestPropertySource; import touch.baton.config.RestdocsConfig; +import touch.baton.domain.member.Member; import touch.baton.domain.oauth.controller.OauthController; import touch.baton.domain.oauth.service.OauthService; +import touch.baton.domain.oauth.token.AccessToken; +import touch.baton.domain.oauth.token.ExpireDate; +import touch.baton.domain.oauth.token.RefreshToken; +import touch.baton.domain.oauth.token.Token; +import touch.baton.domain.oauth.token.Tokens; import touch.baton.infra.auth.oauth.github.GithubOauthConfig; +import java.time.LocalDateTime; + import static java.nio.charset.StandardCharsets.UTF_8; import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.mock; +import static org.springframework.http.HttpHeaders.LOCATION; import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; @@ -58,17 +71,29 @@ void github_redirect_auth_code() throws Exception { .andDo(restDocs.document( pathParameters( parameterWithName("oauthType").description("소셜 로그인 타입") + ), + responseHeaders( + headerWithName(LOCATION).description("Oauth 서버 리다이렉트 URL") ) )) .andDo(print()); } + // FIXME: 2023/09/15 RFC2616 버전오류 해결해주세요. + @Disabled @DisplayName("Github 소셜 로그인을 위해 AuthCode 를 받아 SocialToken 으로 교환하여 Github 프로필 정보를 찾아오고 미가입 사용자일 경우 자동으로 회원가입을 진행하고 JWT 로 변환하여 클라이언트에게 넘겨준다.") @Test void github_login() throws Exception { // given & when + final RefreshToken refreshToken = RefreshToken.builder() + .member(mock(Member.class)) + .token(new Token("mock refresh token")) + .expireDate(new ExpireDate(LocalDateTime.now().plusDays(30))) + .build(); + final Tokens tokens = new Tokens(new AccessToken("Bearer Jwt"), refreshToken); + when(oauthService.login(GITHUB, "authcode")) - .thenReturn("Bearer Jwt"); + .thenReturn(tokens); // then mockMvc.perform(get("/api/v1/oauth/login/{oauthType}", "github") @@ -78,16 +103,20 @@ void github_login() throws Exception { ) .andExpect(status().isOk()) .andDo(restDocs.document( - pathParameters( - parameterWithName("oauthType").description("소셜 로그인 타입") - ), - queryParameters( - parameterWithName("code").description("소셜로부터 redirect 하여 받은 AuthCode") - ), - responseHeaders( - headerWithName("Authorization").description("Json Web Token") + pathParameters( + parameterWithName("oauthType").description("소셜 로그인 타입") + ), + queryParameters( + parameterWithName("code").description("소셜로부터 redirect 하여 받은 AuthCode") + ), + responseHeaders( + headerWithName("Authorization").description("발급된 JWT 토큰") + ), + responseCookies( + cookieWithName("refreshToken").description("발급된 리프레시 토큰") + ) ) - )) + ) .andDo(print()); } } diff --git a/backend/baton/src/test/java/touch/baton/document/oauth/token/RefreshTokenApiTest.java b/backend/baton/src/test/java/touch/baton/document/oauth/token/RefreshTokenApiTest.java new file mode 100644 index 000000000..4495eebc5 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/document/oauth/token/RefreshTokenApiTest.java @@ -0,0 +1,91 @@ +package touch.baton.document.oauth.token; + +import jakarta.servlet.http.Cookie; +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.oauth.AuthorizationHeader; +import touch.baton.domain.oauth.controller.OauthController; +import touch.baton.domain.oauth.service.OauthService; +import touch.baton.domain.oauth.token.AccessToken; +import touch.baton.domain.oauth.token.ExpireDate; +import touch.baton.domain.oauth.token.RefreshToken; +import touch.baton.domain.oauth.token.Token; +import touch.baton.domain.oauth.token.Tokens; +import touch.baton.fixture.domain.MemberFixture; + +import java.time.Duration; +import java.time.LocalDateTime; + +import static org.apache.http.HttpHeaders.AUTHORIZATION; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; +import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; +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.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(OauthController.class) +class RefreshTokenApiTest extends RestdocsConfig { + + @MockBean + private OauthService oauthService; + + @BeforeEach + void setUp() { + final OauthController oauthController = new OauthController(oauthService); + restdocsSetUp(oauthController); + } + + @DisplayName("만료된 jwt 토큰과 refresh token 으로 refresh 요청을 하면 새로운 토큰들이 반환된다.") + @Test + void refresh() throws Exception { + // given & when + final RefreshToken refreshToken = RefreshToken.builder() + .token(new Token("refresh-token")) + .member(MemberFixture.createEthan()) + .expireDate(new ExpireDate(LocalDateTime.now().plusDays(30))) + .build(); + final Tokens tokens = new Tokens(new AccessToken("renew access token"), refreshToken); + final Cookie cookie = createCookie(); + + given(oauthService.reissueAccessToken(any(AuthorizationHeader.class), any(String.class))).willReturn(tokens); + + // then + mockMvc.perform(post("/api/v1/oauth/refresh") + .header(AUTHORIZATION, "expired access token") + .cookie(cookie)) + .andExpect(status().isNoContent()) + .andDo(restDocs.document( + requestHeaders( + headerWithName("Authorization").description("만료된 JWT 토큰") + ), + requestCookies( + cookieWithName("refreshToken").description("만료되지 않은 리프레시 토큰") + ), + responseHeaders( + headerWithName("Authorization").description("새로 발급된 JWT 토큰") + ), + responseCookies( + cookieWithName("refreshToken").description("새로 발급된 리프레시 토큰") + ) + )) + .andDo(print()); + } + + private Cookie createCookie() { + final Cookie cookie = new Cookie("refreshToken", "refresh-token"); + cookie.setSecure(true); + cookie.setHttpOnly(true); + cookie.setMaxAge((int) Duration.ofDays(30).toSeconds()); + return cookie; + } +} diff --git a/backend/baton/src/test/java/touch/baton/document/runnerpost/create/RunnerPostCreateApiTest.java b/backend/baton/src/test/java/touch/baton/document/runnerpost/create/RunnerPostCreateApiTest.java index 248011e8d..f42f82783 100644 --- a/backend/baton/src/test/java/touch/baton/document/runnerpost/create/RunnerPostCreateApiTest.java +++ b/backend/baton/src/test/java/touch/baton/document/runnerpost/create/RunnerPostCreateApiTest.java @@ -52,7 +52,9 @@ void createRunnerPost() throws Exception { List.of("Java", "Spring"), "https://github.com/cookienc", LocalDateTime.now().plusDays(10), - "12345".repeat(200) + "12345".repeat(200), + "궁금해 궁금해~", + "참고 부탁해요." ); // when diff --git a/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadAllApiTest.java b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadAllApiTest.java index 10d01b05b..83897c640 100644 --- a/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadAllApiTest.java +++ b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadAllApiTest.java @@ -63,32 +63,36 @@ void setUp() { @DisplayName("러너 게시글 전체 조회 API") @Test - void readAllRunnerPosts() throws Exception { + void readRunnerPostsByReviewStatus() throws Exception { // given final Runner runner = RunnerFixture.createRunner(MemberFixture.createHyena()); final Deadline deadline = deadline(LocalDateTime.now().plusHours(100)); final Tag javaTag = TagFixture.create(tagName("자바")); final RunnerPost runnerPost = RunnerPostFixture.create(runner, deadline, List.of(javaTag)); final RunnerPost spyRunnerPost = spy(runnerPost); + final ReviewStatus reviewStatus = ReviewStatus.IN_PROGRESS; given(spyRunnerPost.getId()).willReturn(1L); // when final List runnerPosts = List.of(spyRunnerPost); final PageRequest pageOne = PageRequest.of(1, 10); final PageImpl pageRunnerPosts = new PageImpl<>(runnerPosts, pageOne, runnerPosts.size()); - when(runnerPostService.readAllRunnerPosts(any())).thenReturn(pageRunnerPosts); + when(spyRunnerPost.getReviewStatus()).thenReturn(reviewStatus); + when(runnerPostService.readRunnerPostsByReviewStatus(any(), any())).thenReturn(pageRunnerPosts); when(runnerPostService.readCountsByRunnerPostIds(anyList())).thenReturn(List.of(1L)); // then mockMvc.perform(get("/api/v1/posts/runner") .queryParam("size", String.valueOf(pageOne.getPageSize())) - .queryParam("page", String.valueOf(pageOne.getPageNumber()))) + .queryParam("page", String.valueOf(pageOne.getPageNumber())) + .queryParam("reviewStatus", String.valueOf(reviewStatus))) .andExpect(status().isOk()) .andExpect(content().contentType(APPLICATION_JSON)) .andDo(restDocs.document( queryParameters( parameterWithName("size").description("페이지 사이즈"), - parameterWithName("page").description("페이지 번호") + parameterWithName("page").description("페이지 번호"), + parameterWithName("reviewStatus").description("리뷰 상태") ), responseFields( fieldWithPath("data.[].runnerPostId").type(NUMBER).description("러너 게시글 식별자값(id)"), @@ -165,6 +169,7 @@ void readRunnerMyPage() throws Exception { fieldWithPath("data.[].watchedCount").type(NUMBER).description("러너 게시글의 조회수"), fieldWithPath("data.[].applicantCount").type(NUMBER).description("러너 게시글에 신청한 서포터 수"), fieldWithPath("data.[].reviewStatus").type(STRING).description("러너 게시글 리뷰 상태"), + fieldWithPath("data.[].isReviewed").type(BOOLEAN).description("러너 게시글을 리뷰해준 서포터에 대한 피드백 유무"), fieldWithPath("pageInfo.isFirst").type(BOOLEAN).description("첫 번째 페이지인지"), fieldWithPath("pageInfo.isLast").type(BOOLEAN).description("마지막 페이지 인지"), fieldWithPath("pageInfo.hasNext").type(BOOLEAN).description("다음 페이지가 있는지"), diff --git a/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadOneApiTest.java b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadOneApiTest.java index d95fda106..5dd15fe5f 100644 --- a/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadOneApiTest.java +++ b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadOneApiTest.java @@ -30,10 +30,7 @@ import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -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.JsonFieldType.*; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; @@ -100,8 +97,10 @@ void readByRunnerPostId() throws Exception { responseFields( fieldWithPath("runnerPostId").type(NUMBER).description("러너 게시글 식별자값(id)"), fieldWithPath("title").type(STRING).description("러너 게시글 제목"), - fieldWithPath("contents").type(STRING).description("러너 게시글 내용"), - fieldWithPath("deadline").type(STRING).description("러너 게시글 마감기한"), + fieldWithPath("implementedContents").type(STRING).description("구현 내용"), + fieldWithPath("curiousContents").type(STRING).description("궁금한 점"), + fieldWithPath("postscriptContents").type(STRING).description("참고 사항"), + fieldWithPath("deadline").type(STRING).description("러너 게시글 마감 기한"), fieldWithPath("isOwner").type(BOOLEAN).description("러너 게시글 주인 여부"), fieldWithPath("isApplied").type(BOOLEAN).description("로그인한 서포터 리뷰 지원 여부"), fieldWithPath("applicantCount").type(NUMBER).description("러너 게시글 서포터 지원자수"), diff --git a/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadSearchApiTest.java b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadSearchApiTest.java new file mode 100644 index 000000000..da8d9975e --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadSearchApiTest.java @@ -0,0 +1,125 @@ +package touch.baton.document.runnerpost.read; + +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 org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import touch.baton.config.RestdocsConfig; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.controller.RunnerPostReadController; +import touch.baton.domain.runnerpost.repository.dto.ApplicantCountMappingDto; +import touch.baton.domain.runnerpost.service.RunnerPostReadService; +import touch.baton.domain.runnerpost.service.RunnerPostService; +import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.ReviewStatus; +import touch.baton.domain.tag.Tag; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.RunnerFixture; +import touch.baton.fixture.domain.RunnerPostFixture; +import touch.baton.fixture.domain.TagFixture; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +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; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static touch.baton.fixture.vo.DeadlineFixture.deadline; +import static touch.baton.fixture.vo.TagNameFixture.tagName; + +@WebMvcTest(RunnerPostReadController.class) +class RunnerPostReadSearchApiTest extends RestdocsConfig { + + @MockBean + private RunnerPostReadService runnerPostReadService; + + @MockBean + private RunnerPostService runnerPostService; + + @BeforeEach + void setUp() { + final RunnerPostReadController runnerPostReadController = new RunnerPostReadController(runnerPostReadService, runnerPostService); + restdocsSetUp(runnerPostReadController); + } + + @DisplayName("태그 이름과 리뷰 상태를 조건으로 러너 게시글 페이징 조회 API") + @Test + void readRunnerPostsByTagNamesAndReviewStatus() throws Exception { + // given + final Runner runner = RunnerFixture.createRunner(MemberFixture.createHyena()); + + final Deadline deadline = deadline(LocalDateTime.now().plusHours(100)); + final Tag javaTag = TagFixture.create(tagName("자바")); + final Tag springTag = TagFixture.create(tagName("스프링")); + final RunnerPost runnerPost = RunnerPostFixture.create(runner, deadline, List.of(javaTag, springTag)); + + final RunnerPost spyRunnerPost = spy(runnerPost); + given(spyRunnerPost.getId()).willReturn(1L); + + // when + final List runnerPosts = List.of(spyRunnerPost); + final PageRequest pageOne = PageRequest.of(1, 10); + final PageImpl pageRunnerPosts = new PageImpl<>(runnerPosts, pageOne, runnerPosts.size()); + when(runnerPostReadService.readRunnerPostByTagNameAndReviewStatus(any(Pageable.class), anyString(), any(ReviewStatus.class))) + .thenReturn(pageRunnerPosts); + + when(runnerPostReadService.readApplicantCountMappingByRunnerPostIds(anyList())) + .thenReturn(new ApplicantCountMappingDto(Map.of(1L, 0L))); + + // then + mockMvc.perform(get("/api/v1/posts/runner/tags/search") + .queryParam("size", String.valueOf(pageOne.getPageSize())) + .queryParam("page", String.valueOf(pageOne.getPageNumber())) + .queryParam("reviewStatus", ReviewStatus.NOT_STARTED.name()) + .queryParam("tagName", javaTag.getTagName().getValue())) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andDo(restDocs.document( + queryParameters( + parameterWithName("size").description("페이지 사이즈"), + parameterWithName("page").description("페이지 번호"), + parameterWithName("reviewStatus").description("리뷰 상태"), + parameterWithName("tagName").description("태그 이름") + ), + responseFields( + fieldWithPath("data.[].runnerPostId").type(NUMBER).description("러너 게시글 식별자값(id)"), + fieldWithPath("data.[].title").type(STRING).description("러너 게시글의 제목"), + fieldWithPath("data.[].deadline").type(STRING).description("러너 게시글의 마감 기한"), + fieldWithPath("data.[].watchedCount").type(NUMBER).description("러너 게시글의 조회수"), + fieldWithPath("data.[].applicantCount").type(NUMBER).description("러너 게시글에 신청한 서포터 수"), + fieldWithPath("data.[].reviewStatus").type(STRING).description("러너 게시글의 리뷰 상태"), + fieldWithPath("data.[].runnerProfile.name").type(STRING).description("러너 게시글의 러너 프로필 이름"), + fieldWithPath("data.[].runnerProfile.imageUrl").type(STRING).description("러너 게시글의 러너 프로필 이미지"), + fieldWithPath("data.[].tags.[]").type(ARRAY).description("러너 게시글의 태그 목록"), + fieldWithPath("pageInfo.isFirst").type(BOOLEAN).description("첫 번째 페이지인지"), + fieldWithPath("pageInfo.isLast").type(BOOLEAN).description("마지막 페이지인지"), + fieldWithPath("pageInfo.hasNext").type(BOOLEAN).description("다음 페이지가 있는지"), + fieldWithPath("pageInfo.totalPages").type(NUMBER).description("총 페이지 수"), + fieldWithPath("pageInfo.totalElements").type(NUMBER).description("총 데이터 수"), + fieldWithPath("pageInfo.currentPage").type(NUMBER).description("현재 페이지"), + fieldWithPath("pageInfo.currentSize").type(NUMBER).description("현재 페이지 데이터 수") + )) + ); + } +} diff --git a/backend/baton/src/test/java/touch/baton/document/runnerpost/read/TagReadApiTest.java b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/TagReadApiTest.java new file mode 100644 index 000000000..721b370e6 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/TagReadApiTest.java @@ -0,0 +1,77 @@ +package touch.baton.document.runnerpost.read; + +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.tag.Tag; +import touch.baton.domain.tag.controller.TagController; +import touch.baton.domain.tag.service.TagService; +import touch.baton.fixture.domain.TagFixture; + +import java.util.List; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +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; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static touch.baton.fixture.vo.TagNameFixture.tagName; + +@WebMvcTest(TagController.class) +class TagReadApiTest extends RestdocsConfig { + + @MockBean + private TagService tagService; + + @BeforeEach + void setUp() { + final TagController tagController = new TagController(tagService); + restdocsSetUp(tagController); + } + + @DisplayName("태그 검색 API") + @Test + void readTagsByReducedName() throws Exception { + // given + final Tag javaTag = TagFixture.create(tagName("java")); + final Tag javascriptTag = TagFixture.create(tagName("javascript")); + final Tag javaTagSpy = spy(javaTag); + final Tag javascriptTagSpy = spy(javascriptTag); + + // when + when(tagService.readTagsByReducedName("java")) + .thenReturn(List.of(javaTagSpy, javascriptTagSpy)); + when(javaTagSpy.getId()) + .thenReturn(1L); + when(javascriptTagSpy.getId()) + .thenReturn(2L); + + // then + mockMvc.perform(get("/api/v1/tags/search") + .characterEncoding(UTF_8) + .queryParam("tagName", "java")) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andDo(restDocs.document( + queryParameters( + parameterWithName("tagName").description("태그 이름") + ), + responseFields( + fieldWithPath("data.[].id").type(NUMBER).description("태그 식별자값(id)"), + fieldWithPath("data.[].tagName").type(STRING).optional().description("태그 이름") + )) + ); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/common/vo/TagNameTest.java b/backend/baton/src/test/java/touch/baton/domain/common/vo/TagNameTest.java new file mode 100644 index 000000000..fce47f9b2 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/common/vo/TagNameTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.common.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class TagNameTest { + + @DisplayName("value 가 null 이면 예외가 발생한다.") + @Test + void fail_if_value_is_null() { + assertThatThrownBy(() -> new TagName(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/feedback/repository/SupporterFeedbackRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/feedback/repository/SupporterFeedbackRepositoryTest.java new file mode 100644 index 000000000..acbb188fe --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/feedback/repository/SupporterFeedbackRepositoryTest.java @@ -0,0 +1,65 @@ +package touch.baton.domain.feedback.repository; + +import jakarta.persistence.EntityManager; +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.feedback.SupporterFeedback; +import touch.baton.domain.member.Member; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runnerpost.RunnerPost; +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.SupporterFeedbackFixture; +import touch.baton.fixture.domain.SupporterFixture; +import touch.baton.fixture.vo.DeadlineFixture; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class SupporterFeedbackRepositoryTest extends RepositoryTestConfig { + + @Autowired + private EntityManager em; + @Autowired + private SupporterFeedbackRepository supporterFeedbackRepository; + + @DisplayName("러너 게시글 아이디와 서포터 아이디로 서포터 피드백 존재 유무를 확인할 수 있다.") + @Test + void existsByRunnerPostIdAndSupporterId() { + // given + final Member runnerMember = MemberFixture.createEthan(); + em.persist(runnerMember); + final Runner runner = RunnerFixture.createRunner(runnerMember); + em.persist(runner); + + final Member reviewedSupporterMember = MemberFixture.createHyena(); + em.persist(reviewedSupporterMember); + final Supporter reviewedSupporter = SupporterFixture.create(reviewedSupporterMember); + em.persist(reviewedSupporter); + + final RunnerPost runnerPost = RunnerPostFixture.create(runner, DeadlineFixture.deadline(LocalDateTime.now().plusDays(10))); + em.persist(runnerPost); + + final SupporterFeedback supporterFeedback = SupporterFeedbackFixture.create(reviewedSupporter, runner, runnerPost); + em.persist(supporterFeedback); + + final Member notReviewedSupporterMember = MemberFixture.createHyena(); + em.persist(notReviewedSupporterMember); + final Supporter notReviewedSupporter = SupporterFixture.create(notReviewedSupporterMember); + em.persist(notReviewedSupporter); + + em.flush(); + em.close(); + + // when, then + assertSoftly(softly -> { + softly.assertThat(supporterFeedbackRepository.existsByRunnerPostIdAndSupporterId(runnerPost.getId(), reviewedSupporter.getId())).isTrue(); + softly.assertThat(supporterFeedbackRepository.existsByRunnerPostIdAndSupporterId(runnerPost.getId(), notReviewedSupporter.getId())).isFalse(); + }); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/feedback/service/FeedbackServiceTest.java b/backend/baton/src/test/java/touch/baton/domain/feedback/service/FeedbackServiceTest.java index 06fd43b44..7dce9f61c 100644 --- a/backend/baton/src/test/java/touch/baton/domain/feedback/service/FeedbackServiceTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/feedback/service/FeedbackServiceTest.java @@ -17,8 +17,8 @@ import java.util.ArrayList; import java.util.List; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; class FeedbackServiceTest extends ServiceTestConfig { @@ -26,17 +26,18 @@ class FeedbackServiceTest extends ServiceTestConfig { private Runner exactRunner; private RunnerPost runnerPost; private SupporterFeedBackCreateRequest request; + private Supporter reviewedSupporter; @BeforeEach void setUp() { feedbackService = new FeedbackService(supporterFeedbackRepository, runnerPostRepository, supporterRepository); - Member ethan = memberRepository.save(MemberFixture.createEthan()); + final Member ethan = memberRepository.save(MemberFixture.createEthan()); exactRunner = runnerRepository.save(RunnerFixture.createRunner(ethan)); - Member ditoo = memberRepository.save(MemberFixture.createDitoo()); - Supporter supporterDitoo = supporterRepository.save(SupporterFixture.create(ditoo)); - runnerPost = runnerPostRepository.save(RunnerPostFixture.create(exactRunner, supporterDitoo)); + final Member ditoo = memberRepository.save(MemberFixture.createDitoo()); + reviewedSupporter = supporterRepository.save(SupporterFixture.create(ditoo)); + runnerPost = runnerPostRepository.save(RunnerPostFixture.create(exactRunner, reviewedSupporter)); - request = new SupporterFeedBackCreateRequest("GOOD", List.of("코드리뷰가 맛있어요.", "말투가 친절해요."), supporterDitoo.getId(), runnerPost.getId()); + request = new SupporterFeedBackCreateRequest("GOOD", List.of("코드리뷰가 맛있어요.", "말투가 친절해요."), reviewedSupporter.getId(), runnerPost.getId()); } @DisplayName("러너가 서포터 피드백을 할 수 있다.") @@ -46,7 +47,10 @@ void createSupporterFeedback() { final Long expected = feedbackService.createSupporterFeedback(exactRunner, request); // then - assertThat(expected).isNotNull(); + assertSoftly(softly -> { + softly.assertThat(expected).isNotNull(); + softly.assertThat(runnerPost.getIsReviewed().getValue()).isTrue(); + }); } @DisplayName("소유자가 아닌 러너는 피드백을 할 수 없다.") @@ -58,8 +62,7 @@ void fail_createSupporterFeedback_if_not_owner_runner() { // when, then assertThatThrownBy(() -> feedbackService.createSupporterFeedback(notOwner, request)) - .isInstanceOf(FeedbackBusinessException.class) - .hasMessage("리뷰 글을 작성한 주인만 글을 작성할 수 있습니다."); + .isInstanceOf(FeedbackBusinessException.class); } @DisplayName("리뷰를 하지 않은 서포터를 피드백을 할 수 없다.") @@ -72,7 +75,18 @@ void fail_createSupporterFeedback_if_not_review_supporter_runner() { // when, then assertThatThrownBy(() -> feedbackService.createSupporterFeedback(exactRunner, notReviewSupporterRequest)) - .isInstanceOf(FeedbackBusinessException.class) - .hasMessage("리뷰를 작성한 서포터에 대해서만 피드백을 작성할 수 있습니다."); + .isInstanceOf(FeedbackBusinessException.class); + } + + @DisplayName("이미 서포터 피드백을 작성했으면 서포터 피드백을 할 수 없다.") + @Test + void fail_createSupporterFeedback_if_already_reviewed_supporter() { + // given + final SupporterFeedBackCreateRequest supporterFeedBackCreateRequest = new SupporterFeedBackCreateRequest("GOOD", new ArrayList<>(), reviewedSupporter.getId(), runnerPost.getId()); + feedbackService.createSupporterFeedback(exactRunner, supporterFeedBackCreateRequest); + + // when, then + assertThatThrownBy(() -> feedbackService.createSupporterFeedback(exactRunner, supporterFeedBackCreateRequest)) + .isInstanceOf(FeedbackBusinessException.class); } } diff --git a/backend/baton/src/test/java/touch/baton/domain/oauth/repository/RefreshTokenRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/oauth/repository/RefreshTokenRepositoryTest.java new file mode 100644 index 000000000..b4de24330 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/oauth/repository/RefreshTokenRepositoryTest.java @@ -0,0 +1,92 @@ +package touch.baton.domain.oauth.repository; + +import jakarta.persistence.EntityManager; +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.oauth.token.RefreshToken; +import touch.baton.domain.oauth.token.Token; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.RefreshTokenFixture; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static java.time.LocalDateTime.now; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static touch.baton.fixture.vo.ExpireDateFixture.expireDate; +import static touch.baton.fixture.vo.TokenFixture.token; +import static touch.baton.util.TestDateFormatUtil.createExpireDate; + +class RefreshTokenRepositoryTest extends RepositoryTestConfig { + + @Autowired + private RefreshTokenRepository refreshTokenRepository; + + @Autowired + private EntityManager em; + + @DisplayName("리프레시 토큰을 토큰으로 찾을 수 있다.") + @Test + void findByToken() { + // given + final Member ethan = MemberFixture.createEthan(); + final Member ditoo = MemberFixture.createDitoo(); + em.persist(ethan); + em.persist(ditoo); + + final LocalDateTime expireDate = createExpireDate(now().plusDays(30)); + + final Token ethanToken = token("ethan RefreshToken"); + final RefreshToken expected = RefreshTokenFixture.create(ethan, ethanToken, expireDate(expireDate)); + final Token ditooToken = token("ditoo RefreshToken"); + final RefreshToken otherToken = RefreshTokenFixture.create(ditoo, ditooToken, expireDate(expireDate)); + em.persist(expected); + em.persist(otherToken); + + em.flush(); + em.clear(); + + // when + final Optional actual = refreshTokenRepository.findByToken(ethanToken); + + // then + assertSoftly(softly -> { + softly.assertThat(actual).isPresent(); + softly.assertThat(actual.get().getToken()).isEqualTo(expected.getToken()); + softly.assertThat(actual.get().getExpireDate()).isEqualTo(expected.getExpireDate()); + } ); + } + + @DisplayName("리프레시 토큰을 사용자로 찾을 수 있다.") + @Test + void findByMember() { + // given + final Member owner = MemberFixture.createEthan(); + final Member notOwner = MemberFixture.createDitoo(); + em.persist(owner); + em.persist(notOwner); + + final LocalDateTime expireDate = createExpireDate(now().plusDays(30)); + + final RefreshToken expected = RefreshTokenFixture.create(owner, token("ethan RefreshToken"), expireDate(expireDate)); + final RefreshToken differentRefreshToken = RefreshTokenFixture.create(notOwner, token("ditoo RefreshToken"), expireDate(expireDate)); + em.persist(expected); + em.persist(differentRefreshToken); + + em.flush(); + em.clear(); + + // when + final Optional actual = refreshTokenRepository.findByMember(owner); + + // then + assertSoftly(softly -> { + softly.assertThat(actual).isPresent(); + softly.assertThat(actual.get().getToken()).isEqualTo(expected.getToken()); + softly.assertThat(actual.get().getExpireDate()).isEqualTo(expected.getExpireDate()); + } ); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/oauth/service/OauthServiceUpdateTest.java b/backend/baton/src/test/java/touch/baton/domain/oauth/service/OauthServiceUpdateTest.java new file mode 100644 index 000000000..f13ce375a --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/oauth/service/OauthServiceUpdateTest.java @@ -0,0 +1,184 @@ +package touch.baton.domain.oauth.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import touch.baton.domain.member.Member; +import touch.baton.domain.member.vo.SocialId; +import touch.baton.domain.oauth.AuthorizationHeader; +import touch.baton.domain.oauth.authcode.AuthCodeRequestUrlProviderComposite; +import touch.baton.domain.oauth.client.OauthInformationClientComposite; +import touch.baton.domain.oauth.exception.OauthRequestException; +import touch.baton.domain.oauth.repository.OauthMemberRepository; +import touch.baton.domain.oauth.repository.OauthRunnerRepository; +import touch.baton.domain.oauth.repository.OauthSupporterRepository; +import touch.baton.domain.oauth.repository.RefreshTokenRepository; +import touch.baton.domain.oauth.token.ExpireDate; +import touch.baton.domain.oauth.token.RefreshToken; +import touch.baton.domain.oauth.token.Token; +import touch.baton.domain.oauth.token.Tokens; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.infra.auth.jwt.JwtConfig; +import touch.baton.infra.auth.jwt.JwtDecoder; +import touch.baton.infra.auth.jwt.JwtEncoder; + +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static touch.baton.fixture.vo.AuthorizationHeaderFixture.bearerAuthorizationHeader; + +@ExtendWith(MockitoExtension.class) +class OauthServiceUpdateTest { + + private OauthService oauthService; + @Mock + private AuthCodeRequestUrlProviderComposite authCodeRequestUrlProviderComposite; + @Mock + private OauthInformationClientComposite oauthInformationClientComposite; + @Mock + private OauthMemberRepository oauthMemberRepository; + @Mock + private OauthRunnerRepository oauthRunnerRepository; + @Mock + private OauthSupporterRepository oauthSupporterRepository; + @Mock + private RefreshTokenRepository refreshTokenRepository; + private JwtEncoder jwtEncoder; + private JwtEncoder expiredJwtEncoder; + private JwtDecoder jwtDecoder; + + @BeforeEach + void setUp() { + final JwtConfig normalJwtConfig = new JwtConfig("secret-key-secret-key-secret-key-secret-key-secret-key-secret-key", "test-issuer", 30); + jwtDecoder = new JwtDecoder(normalJwtConfig); + jwtEncoder = new JwtEncoder(normalJwtConfig); + + oauthService = new OauthService(authCodeRequestUrlProviderComposite, oauthInformationClientComposite, oauthMemberRepository, oauthRunnerRepository, oauthSupporterRepository, refreshTokenRepository, jwtEncoder, jwtDecoder); + + final JwtConfig expiredJwtConfig = new JwtConfig("secret-key-secret-key-secret-key-secret-key-secret-key-secret-key", "test-issuer", -1); + expiredJwtEncoder = new JwtEncoder(expiredJwtConfig); + } + + @DisplayName("만료된 jwt 와 만료되지 않은 refreshToken 이 주어지면 토큰들이 정상 발급된다.") + @Test + void success_reissueAccessToken() { + // given + final Member tokenOwner = MemberFixture.createEthan(); + final AuthorizationHeader expiredAuthorizationHeader = bearerAuthorizationHeader(expiredJwtEncoder.jwtToken(Map.of("socialId", tokenOwner.getSocialId().getValue()))); + final String refreshTokenValue = "ethan-refresh-token"; + final RefreshToken beforeRefreshToken = RefreshToken.builder() + .member(tokenOwner) + .token(new Token(refreshTokenValue)) + .expireDate(new ExpireDate(LocalDateTime.now().plusDays(30))) + .build(); + + given(oauthMemberRepository.findBySocialId(eq(new SocialId(tokenOwner.getSocialId().getValue())))).willReturn(Optional.of(tokenOwner)); + given(refreshTokenRepository.findByToken(eq(new Token(refreshTokenValue)))).willReturn(Optional.of(beforeRefreshToken)); + + // when + final Tokens tokens = oauthService.reissueAccessToken(expiredAuthorizationHeader, refreshTokenValue); + + // then + assertSoftly(softly -> { + softly.assertThat(tokens.accessToken()).isNotNull(); + softly.assertThat(tokens.accessToken().getValue()).isNotEqualTo(expiredAuthorizationHeader.parseBearerAccessToken()); + softly.assertThat(tokens.refreshToken()).isNotNull(); + softly.assertThat(tokens.refreshToken().getToken().getValue()).isNotEqualTo(refreshTokenValue); + }); + } + + @DisplayName("만료되지 않은 jwt 로 재발급 요청하면 오류가 발생한다.") + @Test + void fail_reissueAccessToken_when_jwt_is_not_expired() { + // given + final Member tokenOwner = MemberFixture.createEthan(); + final AuthorizationHeader normalJwtToken = bearerAuthorizationHeader(jwtEncoder.jwtToken(Map.of("socialId", tokenOwner.getSocialId().getValue()))); + final String refreshTokenValue = "ethan-refresh-token"; + final RefreshToken beforeRefreshToken = RefreshToken.builder() + .member(tokenOwner) + .token(new Token(refreshTokenValue)) + .expireDate(new ExpireDate(LocalDateTime.now().plusDays(30))) + .build(); + + // when, then + assertThatThrownBy(() -> oauthService.reissueAccessToken(normalJwtToken, refreshTokenValue)).isInstanceOf(OauthRequestException.class); + } + + @DisplayName("없는 socialId 가 재발급 요청하면 오류가 발생한다.") + @Test + void fail_reissueAccessToken_when_socialId_not_exists() { + // given + final Member tokenOwner = MemberFixture.createEthan(); + final AuthorizationHeader expiredAuthorizationHeader = bearerAuthorizationHeader(expiredJwtEncoder.jwtToken(Map.of("socialId", tokenOwner.getSocialId().getValue()))); + final String refreshTokenValue = "ethan-refresh-token"; + + given(oauthMemberRepository.findBySocialId(eq(new SocialId(tokenOwner.getSocialId().getValue())))).willReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> oauthService.reissueAccessToken(expiredAuthorizationHeader, refreshTokenValue)).isInstanceOf(OauthRequestException.class); + } + + @DisplayName("refreshToken 이 없으면 재발급 요청하면 오류가 발생한다.") + @Test + void fail_reissueAccessToken_when_refreshToken_not_exists() { + // given + final Member tokenOwner = MemberFixture.createEthan(); + final AuthorizationHeader expiredAuthorizationHeader = bearerAuthorizationHeader(expiredJwtEncoder.jwtToken(Map.of("socialId", tokenOwner.getSocialId().getValue()))); + final String refreshTokenValue = "ethan-refresh-token"; + + given(oauthMemberRepository.findBySocialId(eq(new SocialId(tokenOwner.getSocialId().getValue())))).willReturn(Optional.of(tokenOwner)); + given(refreshTokenRepository.findByToken(eq(new Token(refreshTokenValue)))).willReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> oauthService.reissueAccessToken(expiredAuthorizationHeader, refreshTokenValue)).isInstanceOf(OauthRequestException.class); + } + + @DisplayName("주인이 아닌 accessToken 으로 재발급 요청하면 오류가 발생한다.") + @Test + void fail_reissueAccessToken_when_not_owner_of_accessToken() { + // given + final Member tokenOwner = MemberFixture.createEthan(); + final AuthorizationHeader expiredAuthorizationHeader = bearerAuthorizationHeader(expiredJwtEncoder.jwtToken(Map.of("socialId", tokenOwner.getSocialId().getValue()))); + final String refreshTokenValue = "ethan-refresh-token"; + final RefreshToken beforeRefreshToken = RefreshToken.builder() + .member(tokenOwner) + .token(new Token(refreshTokenValue)) + .expireDate(new ExpireDate(LocalDateTime.now().plusDays(30))) + .build(); + + final Member notTokenOwner = MemberFixture.createHyena(); + given(oauthMemberRepository.findBySocialId(eq(new SocialId(tokenOwner.getSocialId().getValue())))).willReturn(Optional.of(notTokenOwner)); + given(refreshTokenRepository.findByToken(eq(new Token(refreshTokenValue)))).willReturn(Optional.of(beforeRefreshToken)); + + // when, then + assertThatThrownBy(() -> oauthService.reissueAccessToken(expiredAuthorizationHeader, refreshTokenValue)).isInstanceOf(OauthRequestException.class); + } + + @DisplayName("만료된 refreshToken 으로 재발급 요청하면 오류가 발생한다.") + @Test + void fail_reissueAccessToken_when_refreshToken_is_expired() { + // given + final Member tokenOwner = MemberFixture.createEthan(); + final AuthorizationHeader expiredAuthorizationHeader = bearerAuthorizationHeader(expiredJwtEncoder.jwtToken(Map.of("socialId", tokenOwner.getSocialId().getValue()))); + final String refreshTokenValue = "ethan-refresh-token"; + final RefreshToken beforeRefreshToken = RefreshToken.builder() + .member(tokenOwner) + .token(new Token(refreshTokenValue)) + .expireDate(new ExpireDate(LocalDateTime.now().minusDays(14))) + .build(); + + given(oauthMemberRepository.findBySocialId(eq(new SocialId(tokenOwner.getSocialId().getValue())))).willReturn(Optional.of(tokenOwner)); + given(refreshTokenRepository.findByToken(eq(new Token(refreshTokenValue)))).willReturn(Optional.of(beforeRefreshToken)); + + // when, then + assertThatThrownBy(() -> oauthService.reissueAccessToken(expiredAuthorizationHeader, refreshTokenValue)).isInstanceOf(OauthRequestException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/oauth/token/RefreshTokenTest.java b/backend/baton/src/test/java/touch/baton/domain/oauth/token/RefreshTokenTest.java new file mode 100644 index 000000000..0d3e7c5ba --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/oauth/token/RefreshTokenTest.java @@ -0,0 +1,159 @@ +package touch.baton.domain.oauth.token; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import touch.baton.domain.member.Member; +import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.vo.ImageUrl; +import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.vo.OauthId; +import touch.baton.domain.member.vo.SocialId; +import touch.baton.domain.oauth.token.exception.RefreshTokenDomainException; + +import java.time.LocalDateTime; + +import static java.time.LocalDateTime.now; +import static java.time.temporal.ChronoUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static touch.baton.domain.oauth.token.RefreshToken.builder; + +class RefreshTokenTest { + + private static final Member owner = Member.builder() + .memberName(new MemberName("러너 사용자")) + .socialId(new SocialId("testSocialId")) + .oauthId(new OauthId("ads7821iuqjkrhadsioh1f1r4efsoi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한테크코스")) + .imageUrl(new ImageUrl("김석호")) + .build(); + + @DisplayName("생성 테스트") + @Nested + class Create { + + @DisplayName("성공한다.") + @Test + void success() { + assertThatCode(() -> builder() + .member(owner) + .token(new Token("refresh-token")) + .expireDate(new ExpireDate(now().plusDays(30))) + .build()); + } + + @DisplayName("사용자가 null 이면 실패한다.") + @Test + void fail_if_member_is_null() { + assertThatThrownBy(() -> RefreshToken.builder() + .member(null) + .token(new Token("refresh-token")) + .expireDate(new ExpireDate(now().plusDays(30))) + .build() + ).isInstanceOf(RefreshTokenDomainException.class); + } + + @DisplayName("토큰이 null 이면 실패한다.") + @Test + void fail_if_token_is_null() { + assertThatThrownBy(() -> RefreshToken.builder() + .member(owner) + .token(null) + .expireDate(new ExpireDate(now().plusDays(30))) + .build() + ).isInstanceOf(RefreshTokenDomainException.class); + } + + @DisplayName("만료일이 null 이면 실패한다.") + @Test + void fail_if_expireDate_is_null() { + assertThatThrownBy(() -> RefreshToken.builder() + .member(owner) + .token(new Token("refresh-token")) + .expireDate(null) + .build() + ).isInstanceOf(RefreshTokenDomainException.class); + } + } + + @DisplayName("토큰을 업데이트하면 토큰의 내용과 마감기한이 바뀐다.") + @Test + void updateToken() { + // given + final LocalDateTime currentTime = now(); + final ExpireDate expectedExpireDate = new ExpireDate(currentTime); + final RefreshToken refreshToken = builder() + .member(owner) + .token(new Token("refresh-token")) + .expireDate(expectedExpireDate) + .build(); + + // when + final Token updateToken = new Token("update-token"); + refreshToken.updateToken(updateToken, 30); + + // then + assertAll( + () -> assertThat(refreshToken.getToken()).isEqualTo(updateToken), + () -> assertThat(refreshToken.getExpireDate().getValue().truncatedTo(MINUTES)) + .isEqualTo((currentTime.plusMinutes(30)).truncatedTo(MINUTES))); + } + + @DisplayName("토큰의 주인을 확인할 수 있다.") + @Test + void isNotOwner() { + // given + final LocalDateTime currentTime = now(); + final ExpireDate expectedExpireDate = new ExpireDate(currentTime); + final RefreshToken refreshToken = builder() + .member(owner) + .token(new Token("refresh-token")) + .expireDate(expectedExpireDate) + .build(); + + final Member notOwner = Member.builder() + .memberName(new MemberName("Not Owner")) + .socialId(new SocialId("notOwnerSocialId")) + .oauthId(new OauthId("notOwnerOauthId")) + .githubUrl(new GithubUrl("github.com/notOwner")) + .company(new Company("우아한테크코스")) + .imageUrl(new ImageUrl("김석호")) + .build(); + + // when, then + assertAll( + () -> assertThat(refreshToken.isNotOwner(owner)).isFalse(), + () -> assertThat(refreshToken.isNotOwner(notOwner)).isTrue() + ); + } + + @DisplayName("만료되었는지 확인한다.") + @Test + void isExpired() { + // given + final LocalDateTime currentTime = now().minusDays(20); + final ExpireDate expectedExpireDate = new ExpireDate(currentTime); + final RefreshToken refreshToken = builder() + .member(owner) + .token(new Token("refresh-token")) + .expireDate(expectedExpireDate) + .build(); + + final Member notOwner = Member.builder() + .memberName(new MemberName("Not Owner")) + .socialId(new SocialId("notOwnerSocialId")) + .oauthId(new OauthId("notOwnerOauthId")) + .githubUrl(new GithubUrl("github.com/notOwner")) + .company(new Company("우아한테크코스")) + .imageUrl(new ImageUrl("김석호")) + .build(); + + // when, then + assertThat(refreshToken.isExpired()).isTrue(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/oauth/vo/AuthorizationHeaderTest.java b/backend/baton/src/test/java/touch/baton/domain/oauth/vo/AuthorizationHeaderTest.java new file mode 100644 index 000000000..36271b0b8 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/oauth/vo/AuthorizationHeaderTest.java @@ -0,0 +1,61 @@ +package touch.baton.domain.oauth.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import touch.baton.domain.oauth.AuthorizationHeader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class AuthorizationHeaderTest { + + @DisplayName("생성 테스트") + @Nested + class Create { + + @DisplayName("성공한다.") + @Test + void success() { + assertThatCode(() -> new AuthorizationHeader("hello")) + .doesNotThrowAnyException(); + } + + @DisplayName("value 가 null 이면 예외가 발생한다.") + @Test + void fail_if_value_is_null() { + assertThatThrownBy(() -> new AuthorizationHeader(null)) + .isInstanceOf(IllegalArgumentException.class); + } + } + + @DisplayName("Bearer 로 파싱할 수 있다.") + @Test + void parseBearerAccessToken() { + // given + final String expected = "accessToken"; + final AuthorizationHeader authorizationHeader = new AuthorizationHeader("Bearer " + expected); + + // when + final String actual = authorizationHeader.parseBearerAccessToken(); + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("Bearer 로 시작하는지 검증한다.") + @Test + void isNotBearerAuth() { + // given + final AuthorizationHeader bearerAuthorizationHeader = new AuthorizationHeader("Bearer " + "accessToken"); + final AuthorizationHeader notBearerAuthorizationHeader = new AuthorizationHeader("Basic " + "accessToken"); + + // when, then + assertSoftly(softly -> { + softly.assertThat(bearerAuthorizationHeader.isNotBearerAuth()).isFalse(); + softly.assertThat(notBearerAuthorizationHeader.isNotBearerAuth()).isTrue(); + }); + } +} 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 71d2f8bd3..b49968c4b 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,14 +1,11 @@ package touch.baton.domain.runnerpost; -import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import touch.baton.domain.common.vo.Contents; import touch.baton.domain.common.vo.Title; import touch.baton.domain.common.vo.WatchedCount; import touch.baton.domain.member.Member; @@ -20,7 +17,11 @@ import touch.baton.domain.member.vo.SocialId; import touch.baton.domain.runner.Runner; import touch.baton.domain.runnerpost.exception.RunnerPostDomainException; +import touch.baton.domain.runnerpost.vo.CuriousContents; import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.ImplementedContents; +import touch.baton.domain.runnerpost.vo.IsReviewed; +import touch.baton.domain.runnerpost.vo.PostscriptContents; import touch.baton.domain.runnerpost.vo.PullRequestUrl; import touch.baton.domain.runnerpost.vo.ReviewStatus; import touch.baton.domain.supporter.Supporter; @@ -29,11 +30,7 @@ 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; @@ -42,9 +39,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.junit.jupiter.api.Assertions.assertAll; @@ -84,10 +79,12 @@ class RunnerPostTest { void addAllRunnerPostTags() { // given final String title = "JPA 리뷰 부탁 드려요."; - final String contents = "넘나 어려워요."; + final String implementedContents = "넘나 어려워요."; + final String curiousContents = "이것 궁금해요."; + final String postscriptContents = "잘 부탁드립니다."; final String pullRequestUrl = "https://github.com/cookienc"; final LocalDateTime deadline = LocalDateTime.of(2099, 12, 12, 0, 0); - final RunnerPost runnerPost = RunnerPost.newInstance(title, contents, pullRequestUrl, deadline, runner); + final RunnerPost runnerPost = RunnerPost.newInstance(title, implementedContents, curiousContents, postscriptContents, pullRequestUrl, deadline, runner); final RunnerPostTag java = RunnerPostTag.builder() .tag(Tag.newInstance("Java")) .runnerPost(runnerPost) @@ -101,7 +98,7 @@ void addAllRunnerPostTags() { // when runnerPost.addAllRunnerPostTags(List.of(java, spring)); - List runnerPostTags = runnerPost.getRunnerPostTags().getRunnerPostTags(); + List runnerPostTags = runnerPost.getRunnerPostTags().getRunnerPostTags(); final List actualTagNames = runnerPostTags.stream() .map(runnerPostTag -> runnerPostTag.getTag().getTagName().getValue()) .collect(Collectors.toList()); @@ -122,11 +119,14 @@ class Create { void success() { assertThatCode(() -> RunnerPost.builder() .title(new Title("JPA 정복")) - .contents(new Contents("김영한 짱짱맨")) + .implementedContents(new ImplementedContents("김영한 짱짱맨")) + .curiousContents(new CuriousContents("궁금한 점입니다.")) + .postscriptContents(new PostscriptContents("잘 부탁드립니다.")) .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) .deadline(new Deadline(LocalDateTime.now())) .watchedCount(new WatchedCount(0)) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runner(runner) .supporter(supporter) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) @@ -139,11 +139,14 @@ void success() { void success_if_supporter_is_null() { assertThatCode(() -> RunnerPost.builder() .title(new Title("아이")) - .contents(new Contents("김영한 짱짱맨")) + .implementedContents(new ImplementedContents("김영한 짱짱맨")) + .curiousContents(new CuriousContents("궁금한 점입니다.")) + .postscriptContents(new PostscriptContents("잘 부탁드립니다.")) .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) .deadline(new Deadline(LocalDateTime.now())) .watchedCount(new WatchedCount(0)) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runner(runner) .supporter(null) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) @@ -156,11 +159,14 @@ void success_if_supporter_is_null() { void fail_if_title_is_null() { assertThatThrownBy(() -> RunnerPost.builder() .title(null) - .contents(new Contents("김영한 짱짱맨")) + .implementedContents(new ImplementedContents("김영한 짱짱맨")) + .curiousContents(new CuriousContents("궁금한 점입니다.")) + .postscriptContents(new PostscriptContents("잘 부탁드립니다.")) .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) .deadline(new Deadline(LocalDateTime.now())) .watchedCount(new WatchedCount(0)) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runner(runner) .supporter(supporter) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) @@ -169,22 +175,25 @@ void fail_if_title_is_null() { .hasMessage("RunnerPost 의 title 은 null 일 수 없습니다."); } - @DisplayName("contents 에 null 이 들어갈 경우 예외가 발생한다.") + @DisplayName("implementedContents 에 null 이 들어갈 경우 예외가 발생한다.") @Test void fail_if_contents_is_null() { assertThatThrownBy(() -> RunnerPost.builder() .title(new Title("헤나")) - .contents(null) + .implementedContents(null) + .curiousContents(new CuriousContents("궁금한 점입니다.")) + .postscriptContents(new PostscriptContents("잘 부탁드립니다.")) .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) .deadline(new Deadline(LocalDateTime.now())) .watchedCount(new WatchedCount(0)) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runner(runner) .supporter(supporter) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) .build() ).isInstanceOf(RunnerPostDomainException.class) - .hasMessage("RunnerPost 의 contents 는 null 일 수 없습니다."); + .hasMessage("RunnerPost 의 implementedContents 는 null 일 수 없습니다."); } @DisplayName("pull request url 에 null 이 들어갈 경우 예외가 발생한다.") @@ -192,11 +201,14 @@ void fail_if_contents_is_null() { void fail_if_pullRequestUrl_is_null() { assertThatThrownBy(() -> RunnerPost.builder() .title(new Title("하이하이")) - .contents(new Contents("김영한 짱짱맨")) + .implementedContents(new ImplementedContents("김영한 짱짱맨")) + .curiousContents(new CuriousContents("궁금한 점입니다.")) + .postscriptContents(new PostscriptContents("잘 부탁드립니다.")) .pullRequestUrl(null) .deadline(new Deadline(LocalDateTime.now())) .watchedCount(new WatchedCount(0)) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runner(runner) .supporter(supporter) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) @@ -210,11 +222,14 @@ void fail_if_pullRequestUrl_is_null() { void fail_if_deadline_is_null() { assertThatThrownBy(() -> RunnerPost.builder() .title(new Title("아이")) - .contents(new Contents("김영한 짱짱맨")) + .implementedContents(new ImplementedContents("김영한 짱짱맨")) + .curiousContents(new CuriousContents("궁금한 점입니다.")) + .postscriptContents(new PostscriptContents("잘 부탁드립니다.")) .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) .deadline(null) .watchedCount(new WatchedCount(0)) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runner(runner) .supporter(supporter) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) @@ -228,11 +243,14 @@ void fail_if_deadline_is_null() { void fail_if_watchedCount_is_null() { assertThatThrownBy(() -> RunnerPost.builder() .title(new Title("아이")) - .contents(new Contents("김영한 짱짱맨")) + .implementedContents(new ImplementedContents("김영한 짱짱맨")) + .curiousContents(new CuriousContents("궁금한 점입니다.")) + .postscriptContents(new PostscriptContents("잘 부탁드립니다.")) .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) .deadline(new Deadline(LocalDateTime.now())) .watchedCount(null) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runner(runner) .supporter(supporter) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) @@ -241,16 +259,40 @@ void fail_if_watchedCount_is_null() { .hasMessage("RunnerPost 의 watchedCount 는 null 일 수 없습니다."); } + @DisplayName("is reviewed 에 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_isReviewed_is_null() { + assertThatThrownBy(() -> RunnerPost.builder() + .title(new Title("아이")) + .implementedContents(new ImplementedContents("김영한 짱짱맨")) + .curiousContents(new CuriousContents("궁금한 점입니다.")) + .postscriptContents(new PostscriptContents("잘 부탁드립니다.")) + .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) + .deadline(new Deadline(LocalDateTime.now())) + .watchedCount(new WatchedCount(0)) + .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(null) + .runner(runner) + .supporter(supporter) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .build() + ).isInstanceOf(RunnerPostDomainException.class) + .hasMessage("RunnerPost 의 isReviewed 는 null 일 수 없습니다."); + } + @DisplayName("runner 에 null 이 들어갈 경우 예외가 발생한다.") @Test void fail_if_runner_is_null() { assertThatThrownBy(() -> RunnerPost.builder() .title(new Title("아이")) - .contents(new Contents("김영한 짱짱맨")) + .implementedContents(new ImplementedContents("김영한 짱짱맨")) + .curiousContents(new CuriousContents("궁금한 점입니다.")) + .postscriptContents(new PostscriptContents("잘 부탁드립니다.")) .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) .deadline(new Deadline(LocalDateTime.now())) .watchedCount(new WatchedCount(0)) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runner(null) .supporter(supporter) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) @@ -264,11 +306,14 @@ void fail_if_runner_is_null() { void fail_if_runnerPostTags_is_null() { assertThatThrownBy(() -> RunnerPost.builder() .title(new Title("아이")) - .contents(new Contents("김영한 짱짱맨")) + .implementedContents(new ImplementedContents("김영한 짱짱맨")) + .curiousContents(new CuriousContents("궁금한 점입니다.")) + .postscriptContents(new PostscriptContents("잘 부탁드립니다.")) .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) .deadline(new Deadline(LocalDateTime.now())) .watchedCount(new WatchedCount(0)) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runner(runner) .supporter(supporter) .runnerPostTags(null) @@ -282,15 +327,19 @@ void fail_if_runnerPostTags_is_null() { void createDefaultRunnerPost() { // given final String title = "JPA 리뷰 부탁 드려요."; - final String contents = "넘나 어려워요."; + final String implementedContents = "넘나 어려워요."; + final String curiousContents = "이것 궁금해요."; + final String postscriptContents = "잘 부탁드립니다."; final String pullRequestUrl = "https://github.com/cookienc"; final LocalDateTime deadline = LocalDateTime.of(2099, 12, 12, 0, 0); - final RunnerPost runnerPost = RunnerPost.newInstance(title, contents, pullRequestUrl, deadline, runner); + final RunnerPost runnerPost = RunnerPost.newInstance(title, implementedContents, curiousContents, postscriptContents, pullRequestUrl, deadline, runner); // when, then assertAll( () -> assertThat(runnerPost.getTitle()).isEqualTo(new Title(title)), - () -> assertThat(runnerPost.getContents()).isEqualTo(new Contents(contents)), + () -> assertThat(runnerPost.getImplementedContents()).isEqualTo(new ImplementedContents(implementedContents)), + () -> assertThat(runnerPost.getCuriousContents()).isEqualTo(new CuriousContents(curiousContents)), + () -> assertThat(runnerPost.getPostscriptContents()).isEqualTo(new PostscriptContents(postscriptContents)), () -> assertThat(runnerPost.getPullRequestUrl()).isEqualTo(new PullRequestUrl(pullRequestUrl)), () -> assertThat(runnerPost.getDeadline()).isEqualTo(new Deadline(deadline)), () -> assertThat(runnerPost.getRunnerPostTags()).isNotNull(), @@ -309,11 +358,14 @@ void success_supporter_is_null_and_deadline_is_not_end() { // given final RunnerPost runnerPost = RunnerPost.builder() .title(new Title("JPA 정복")) - .contents(new Contents("김영한 짱짱맨")) + .implementedContents(new ImplementedContents("김영한 짱짱맨")) + .curiousContents(new CuriousContents("궁금한 점입니다.")) + .postscriptContents(new PostscriptContents("잘 부탁드립니다.")) .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) .deadline(new Deadline(LocalDateTime.now().plusHours(100))) .watchedCount(new WatchedCount(0)) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runner(runner) .supporter(null) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) @@ -330,11 +382,14 @@ void fail_supporter_is_not_null() { // given final RunnerPost runnerPost = RunnerPost.builder() .title(new Title("JPA 정복")) - .contents(new Contents("김영한 짱짱맨")) + .implementedContents(new ImplementedContents("김영한 짱짱맨")) + .curiousContents(new CuriousContents("궁금한 점입니다.")) + .postscriptContents(new PostscriptContents("잘 부탁드립니다.")) .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) .deadline(new Deadline(LocalDateTime.now().plusHours(100))) .watchedCount(new WatchedCount(0)) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runner(runner) .supporter(supporter) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) @@ -351,11 +406,14 @@ void fail_deadline_is_already_end() { // given final RunnerPost runnerPost = RunnerPost.builder() .title(new Title("JPA 정복")) - .contents(new Contents("김영한 짱짱맨")) + .implementedContents(new ImplementedContents("김영한 짱짱맨")) + .curiousContents(new CuriousContents("궁금한 점입니다.")) + .postscriptContents(new PostscriptContents("잘 부탁드립니다.")) .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) .deadline(new Deadline(LocalDateTime.now().minusDays(100))) .watchedCount(new WatchedCount(0)) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runner(runner) .supporter(supporter) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) @@ -382,11 +440,14 @@ void success_IN_PROGRESS__to_DONE() { // given final RunnerPost runnerPost = RunnerPost.builder() .title(new Title("러너가 작성하는 리뷰 요청 게시글의 테스트 제목입니다.")) - .contents(new Contents("안녕하세요. 테스트 내용입니다.")) + .implementedContents(new ImplementedContents("안녕하세요. 테스트 내용입니다.")) + .curiousContents(new CuriousContents("궁금한 점입니다.")) + .postscriptContents(new PostscriptContents("잘 부탁드립니다.")) .pullRequestUrl(new PullRequestUrl("https://github.com")) .deadline(new Deadline(LocalDateTime.now().plusHours(100))) .watchedCount(new WatchedCount(0)) .reviewStatus(ReviewStatus.IN_PROGRESS) + .isReviewed(IsReviewed.notReviewed()) .runner(runner) .supporter(supporter) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) @@ -405,11 +466,14 @@ void fail_NOT_STARTED__to_IN_PROGRESS() { // given final RunnerPost runnerPost = RunnerPost.builder() .title(new Title("러너가 작성하는 리뷰 요청 게시글의 테스트 제목입니다.")) - .contents(new Contents("안녕하세요. 테스트 내용입니다.")) + .implementedContents(new ImplementedContents("안녕하세요. 테스트 내용입니다.")) + .curiousContents(new CuriousContents("궁금한 점입니다.")) + .postscriptContents(new PostscriptContents("잘 부탁드립니다.")) .pullRequestUrl(new PullRequestUrl("https://github.com")) .deadline(new Deadline(LocalDateTime.now().plusHours(100))) .watchedCount(new WatchedCount(0)) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runner(runner) .supporter(supporter) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) @@ -426,11 +490,14 @@ void fail_NOT_STARTED__to_DONE() { // given final RunnerPost runnerPost = RunnerPost.builder() .title(new Title("러너가 작성하는 리뷰 요청 게시글의 테스트 제목입니다.")) - .contents(new Contents("안녕하세요. 테스트 내용입니다.")) + .implementedContents(new ImplementedContents("안녕하세요. 테스트 내용입니다.")) + .curiousContents(new CuriousContents("궁금한 점입니다.")) + .postscriptContents(new PostscriptContents("잘 부탁드립니다.")) .pullRequestUrl(new PullRequestUrl("https://github.com")) .deadline(new Deadline(LocalDateTime.now().plusHours(100))) .watchedCount(new WatchedCount(0)) .reviewStatus(ReviewStatus.DONE) + .isReviewed(IsReviewed.notReviewed()) .runner(runner) .supporter(supporter) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) @@ -447,11 +514,14 @@ void fail_DONE_to_NOT_STARTED() { // given final RunnerPost runnerPost = RunnerPost.builder() .title(new Title("러너가 작성하는 리뷰 요청 게시글의 테스트 제목입니다.")) - .contents(new Contents("안녕하세요. 테스트 내용입니다.")) + .implementedContents(new ImplementedContents("안녕하세요. 테스트 내용입니다.")) + .curiousContents(new CuriousContents("궁금한 점입니다.")) + .postscriptContents(new PostscriptContents("잘 부탁드립니다.")) .pullRequestUrl(new PullRequestUrl("https://github.com")) .deadline(new Deadline(LocalDateTime.now().plusHours(100))) .watchedCount(new WatchedCount(0)) .reviewStatus(ReviewStatus.DONE) + .isReviewed(IsReviewed.notReviewed()) .runner(runner) .supporter(supporter) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) @@ -468,11 +538,14 @@ void fail_DONE_to_IN_PROGRESS() { // given final RunnerPost runnerPost = RunnerPost.builder() .title(new Title("러너가 작성하는 리뷰 요청 게시글의 테스트 제목입니다.")) - .contents(new Contents("안녕하세요. 테스트 내용입니다.")) + .implementedContents(new ImplementedContents("안녕하세요. 테스트 내용입니다.")) + .curiousContents(new CuriousContents("궁금한 점입니다.")) + .postscriptContents(new PostscriptContents("잘 부탁드립니다.")) .pullRequestUrl(new PullRequestUrl("https://github.com")) .deadline(new Deadline(LocalDateTime.now().plusHours(100))) .watchedCount(new WatchedCount(0)) .reviewStatus(ReviewStatus.DONE) + .isReviewed(IsReviewed.notReviewed()) .runner(runner) .supporter(supporter) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) @@ -490,11 +563,14 @@ void fail_same_to_same(final ReviewStatus reviewStatus) { // given final RunnerPost runnerPost = RunnerPost.builder() .title(new Title("러너가 작성하는 리뷰 요청 게시글의 테스트 제목입니다.")) - .contents(new Contents("안녕하세요. 테스트 내용입니다.")) + .implementedContents(new ImplementedContents("안녕하세요. 테스트 내용입니다.")) + .curiousContents(new CuriousContents("궁금한 점입니다.")) + .postscriptContents(new PostscriptContents("잘 부탁드립니다.")) .pullRequestUrl(new PullRequestUrl("https://github.com")) .deadline(new Deadline(LocalDateTime.now().plusHours(100))) .watchedCount(new WatchedCount(0)) .reviewStatus(reviewStatus) + .isReviewed(IsReviewed.notReviewed()) .runner(runner) .supporter(supporter) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/RunnerPostReadRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/RunnerPostReadRepositoryTest.java new file mode 100644 index 000000000..888d458c8 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/RunnerPostReadRepositoryTest.java @@ -0,0 +1,231 @@ +package touch.baton.domain.runnerpost.repository; + +import jakarta.persistence.EntityManager; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import touch.baton.config.RepositoryTestConfig; +import touch.baton.domain.member.Member; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.repository.dto.ApplicantCountDto; +import touch.baton.domain.runnerpost.repository.dto.ApplicantCountMappingDto; +import touch.baton.domain.runnerpost.vo.ReviewStatus; +import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.supporter.SupporterRunnerPost; +import touch.baton.domain.tag.RunnerPostTag; +import touch.baton.domain.tag.Tag; +import touch.baton.domain.tag.vo.TagReducedName; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.RunnerFixture; +import touch.baton.fixture.domain.RunnerPostFixture; +import touch.baton.fixture.domain.RunnerPostTagFixture; +import touch.baton.fixture.domain.SupporterFixture; +import touch.baton.fixture.domain.SupporterRunnerPostFixture; +import touch.baton.fixture.domain.TagFixture; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static touch.baton.fixture.vo.DeadlineFixture.deadline; +import static touch.baton.fixture.vo.TagNameFixture.tagName; + +class RunnerPostReadRepositoryTest extends RepositoryTestConfig { + + @Autowired + private RunnerPostReadRepository runnerPostReadRepository; + + @Autowired + private EntityManager em; + + @DisplayName("러너 게시글 식별자값 목록으로 서포터 지원자 수를 조회에 성공한다.") + @Test + void countApplicantsByRunnerPostIds() { + // given + final Runner hyenaRunner = persistRunner(MemberFixture.createHyena()); + final Supporter ditooSupporter = persistSupporter(MemberFixture.createDitoo()); + final Supporter ethanSupporter = persistSupporter(MemberFixture.createEthan()); + final Supporter judySupporter = persistSupporter(MemberFixture.createJudy()); + + final RunnerPost runnerPostOne = persistRunnerPost(hyenaRunner); + final RunnerPost runnerPostTwo = persistRunnerPost(hyenaRunner); + final RunnerPost runnerPostThree = persistRunnerPost(hyenaRunner); + final RunnerPost runnerPostFour = persistRunnerPost(hyenaRunner); + final RunnerPost runnerPostFive = persistRunnerPost(hyenaRunner); + final RunnerPost runnerPostSix = persistRunnerPost(hyenaRunner); + + persistApplicant(ditooSupporter, runnerPostOne); + persistApplicant(ethanSupporter, runnerPostOne); + persistApplicant(judySupporter, runnerPostOne); + + persistApplicant(ditooSupporter, runnerPostTwo); + persistApplicant(ethanSupporter, runnerPostTwo); + + persistApplicant(ditooSupporter, runnerPostThree); + + em.flush(); + em.close(); + + // when + final List actual = runnerPostReadRepository.countApplicantsByRunnerPostIds(List.of( + runnerPostTwo.getId(), + runnerPostThree.getId(), + runnerPostFour.getId(), + runnerPostFive.getId(), + runnerPostSix.getId(), + runnerPostOne.getId() + )); + + // then + final List expected = List.of( + new ApplicantCountDto(runnerPostTwo.getId(), 2L), + new ApplicantCountDto(runnerPostThree.getId(), 1L), + new ApplicantCountDto(runnerPostFour.getId(), 0L), + new ApplicantCountDto(runnerPostFive.getId(), 0L), + new ApplicantCountDto(runnerPostSix.getId(), 0L), + new ApplicantCountDto(runnerPostOne.getId(), 3L) + ); + + assertThat(actual).containsExactlyInAnyOrderElementsOf(expected); + } + + @DisplayName("러너 게시글 식별자값 목록으로 서포터 지원자 수 매핑 정보 조회에 성공한다.") + @Test + void findApplicantCountMappingByRunnerPostIds() { + // given + final Runner hyenaRunner = persistRunner(MemberFixture.createHyena()); + final Supporter ditooSupporter = persistSupporter(MemberFixture.createDitoo()); + final Supporter ethanSupporter = persistSupporter(MemberFixture.createEthan()); + final Supporter judySupporter = persistSupporter(MemberFixture.createJudy()); + + final RunnerPost runnerPostOne = persistRunnerPost(hyenaRunner); + final RunnerPost runnerPostTwo = persistRunnerPost(hyenaRunner); + final RunnerPost runnerPostThree = persistRunnerPost(hyenaRunner); + final RunnerPost runnerPostFour = persistRunnerPost(hyenaRunner); + final RunnerPost runnerPostFive = persistRunnerPost(hyenaRunner); + final RunnerPost runnerPostSix = persistRunnerPost(hyenaRunner); + + persistApplicant(ditooSupporter, runnerPostOne); + persistApplicant(ethanSupporter, runnerPostOne); + persistApplicant(judySupporter, runnerPostOne); + + persistApplicant(ditooSupporter, runnerPostTwo); + persistApplicant(ethanSupporter, runnerPostTwo); + + persistApplicant(ditooSupporter, runnerPostThree); + + em.flush(); + em.close(); + + // when + final ApplicantCountMappingDto actual = runnerPostReadRepository.findApplicantCountMappingByRunnerPostIds(List.of( + runnerPostTwo.getId(), + runnerPostThree.getId(), + runnerPostFour.getId(), + runnerPostFive.getId(), + runnerPostSix.getId(), + runnerPostOne.getId() + )); + + // then + final ApplicantCountMappingDto expected = new ApplicantCountMappingDto(Map.of( + runnerPostTwo.getId(), 2L, + runnerPostThree.getId(), 1L, + runnerPostFour.getId(), 0L, + runnerPostFive.getId(), 0L, + runnerPostSix.getId(), 0L, + runnerPostOne.getId(), 3L + )); + + assertThat(actual).isEqualTo(expected); + } + + private Runner persistRunner(final Member member) { + em.persist(member); + final Runner runner = RunnerFixture.createRunner(member); + em.persist(runner); + + return runner; + } + + private Supporter persistSupporter(final Member member) { + em.persist(member); + final Supporter supporter = SupporterFixture.create(member); + em.persist(supporter); + + return supporter; + } + + private RunnerPost persistRunnerPost(final Runner runner) { + final RunnerPost runnerPostOne = RunnerPostFixture.create(runner, deadline(LocalDateTime.now().plusHours(100))); + em.persist(runnerPostOne); + + return runnerPostOne; + } + + private SupporterRunnerPost persistApplicant(final Supporter supporter, final RunnerPost runnerPost) { + final SupporterRunnerPost applicant = SupporterRunnerPostFixture.create(runnerPost, supporter); + em.persist(applicant); + + return applicant; + } + + @DisplayName("축약된 태그 이름과 리뷰 상태로 러너 게시글 페이징 조회에 성공한다.") + @Test + void findByTagReducedNameAndReviewStatus() { + // given + final Runner hyenaRunner = persistRunner(MemberFixture.createHyena()); + + final RunnerPost runnerPostOne = persistRunnerPost(hyenaRunner); + final RunnerPost runnerPostTwo = persistRunnerPost(hyenaRunner); + final RunnerPost runnerPostThree = persistRunnerPost(hyenaRunner); + final RunnerPost runnerPostFour = persistRunnerPost(hyenaRunner); + final RunnerPost runnerPostFive = persistRunnerPost(hyenaRunner); + final RunnerPost runnerPostSix = persistRunnerPost(hyenaRunner); + + final Tag javaTag = persistTag("자바"); + final Tag springTag = persistTag("스프링"); + + persistRunnerPostTag(runnerPostOne, javaTag); + persistRunnerPostTag(runnerPostTwo, javaTag); + persistRunnerPostTag(runnerPostThree, javaTag); + persistRunnerPostTag(runnerPostFour, springTag); + persistRunnerPostTag(runnerPostFive, javaTag); + persistRunnerPostTag(runnerPostSix, springTag); + + em.flush(); + em.close(); + + // when + final PageRequest pageOne = PageRequest.of(0, 10); + + final Page foundRunnerPosts = runnerPostReadRepository.findByTagReducedNameAndReviewStatus( + pageOne, + TagReducedName.from("자바"), + ReviewStatus.NOT_STARTED + ); + + // then + final List expected = List.of(runnerPostOne, runnerPostTwo, runnerPostThree, runnerPostFive); + + assertThat(foundRunnerPosts.getContent()).isEqualTo(expected); + } + + private Tag persistTag(final String tagName) { + final Tag javaTag = TagFixture.create(tagName(tagName)); + em.persist(javaTag); + + return javaTag; + } + + private RunnerPostTag persistRunnerPostTag(final RunnerPost runnerPost, final Tag tag) { + final RunnerPostTag runnerPostTag = RunnerPostTagFixture.create(runnerPost, tag); + em.persist(runnerPostTag); + + return runnerPostTag; + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/RunnerPostRepositoryDeleteTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/RunnerPostRepositoryDeleteTest.java index 3454d722b..7c953fefa 100644 --- a/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/RunnerPostRepositoryDeleteTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/RunnerPostRepositoryDeleteTest.java @@ -4,7 +4,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import touch.baton.config.RepositoryTestConfig; -import touch.baton.domain.common.vo.Contents; import touch.baton.domain.common.vo.Title; import touch.baton.domain.common.vo.WatchedCount; import touch.baton.domain.member.Member; @@ -18,7 +17,11 @@ import touch.baton.domain.runner.Runner; import touch.baton.domain.runner.repository.RunnerRepository; import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.vo.CuriousContents; import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.ImplementedContents; +import touch.baton.domain.runnerpost.vo.IsReviewed; +import touch.baton.domain.runnerpost.vo.PostscriptContents; import touch.baton.domain.runnerpost.vo.PullRequestUrl; import touch.baton.domain.runnerpost.vo.ReviewStatus; import touch.baton.domain.tag.RunnerPostTags; @@ -63,12 +66,15 @@ void success_deleteByRunnerPostId() { final RunnerPost runnerPost = RunnerPost.builder() .title(new Title("제 코드 리뷰 좀 해주세요!!")) - .contents(new Contents("제 코드는 클린코드가 맞을까요?")) + .implementedContents(new ImplementedContents("제 코드는 클린코드가 맞을까요?")) + .curiousContents(new CuriousContents("저는 클린코드가 궁금해요.")) + .postscriptContents(new PostscriptContents("저 상처 잘 받으니깐 부드럽게 말해주세요.")) .deadline(new Deadline(LocalDateTime.now())) .pullRequestUrl(new PullRequestUrl("https://")) .watchedCount(new WatchedCount(1)) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runner(saveRunner) .supporter(null) .build(); diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/RunnerPostRepositoryReadTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/RunnerPostRepositoryReadTest.java index a418ff785..602ded7f1 100644 --- a/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/RunnerPostRepositoryReadTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/RunnerPostRepositoryReadTest.java @@ -4,7 +4,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import touch.baton.config.RepositoryTestConfig; -import touch.baton.domain.common.vo.Contents; import touch.baton.domain.common.vo.Title; import touch.baton.domain.common.vo.WatchedCount; import touch.baton.domain.member.Member; @@ -18,7 +17,11 @@ import touch.baton.domain.runner.Runner; import touch.baton.domain.runner.repository.RunnerRepository; import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.vo.CuriousContents; import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.ImplementedContents; +import touch.baton.domain.runnerpost.vo.IsReviewed; +import touch.baton.domain.runnerpost.vo.PostscriptContents; import touch.baton.domain.runnerpost.vo.PullRequestUrl; import touch.baton.domain.runnerpost.vo.ReviewStatus; import touch.baton.domain.tag.RunnerPostTags; @@ -64,12 +67,15 @@ void success_deleteByRunnerPostId() { final RunnerPost runnerPost = RunnerPost.builder() .title(new Title("제 코드 리뷰 좀 해주세요!!")) - .contents(new Contents("제 코드는 클린코드가 맞을까요?")) + .implementedContents(new ImplementedContents("제 코드는 클린코드가 맞을까요?")) + .curiousContents(new CuriousContents("저는 클린코드가 궁금해요.")) + .postscriptContents(new PostscriptContents("저 상처 잘 받으니깐 부드럽게 말해주세요.")) .deadline(new Deadline(LocalDateTime.now())) .pullRequestUrl(new PullRequestUrl("https://")) .watchedCount(new WatchedCount(1)) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runner(saveRunner) .supporter(null) .build(); diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/read/RunnerPostRepositoryReadTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/read/RunnerPostRepositoryReadTest.java index 69d29eec0..16ccbc670 100644 --- a/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/read/RunnerPostRepositoryReadTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/read/RunnerPostRepositoryReadTest.java @@ -4,22 +4,22 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; 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.runnerpost.repository.RunnerPostRepository; +import touch.baton.domain.runnerpost.vo.IsReviewed; import touch.baton.domain.tag.RunnerPostTag; import touch.baton.domain.tag.Tag; import touch.baton.domain.tag.repository.RunnerPostTagRepository; -import touch.baton.domain.tag.repository.TagRepository; import touch.baton.fixture.domain.MemberFixture; import touch.baton.fixture.domain.RunnerFixture; import touch.baton.fixture.domain.RunnerPostFixture; import touch.baton.fixture.domain.RunnerPostTagFixture; -import touch.baton.fixture.domain.RunnerPostTagsFixture; import touch.baton.fixture.domain.TagFixture; import java.time.LocalDateTime; @@ -27,62 +27,64 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; import static touch.baton.domain.runnerpost.vo.ReviewStatus.NOT_STARTED; -import static touch.baton.fixture.vo.ContentsFixture.contents; +import static touch.baton.fixture.domain.RunnerPostTagsFixture.runnerPostTags; +import static touch.baton.fixture.vo.CuriousContentsFixture.curiousContents; import static touch.baton.fixture.vo.DeadlineFixture.deadline; +import static touch.baton.fixture.vo.ImplementedContentsFixture.implementedContents; +import static touch.baton.fixture.vo.PostscriptContentsFixture.postscriptContents; import static touch.baton.fixture.vo.PullRequestUrlFixture.pullRequestUrl; import static touch.baton.fixture.vo.TitleFixture.title; import static touch.baton.fixture.vo.WatchedCountFixture.watchedCount; +import static touch.baton.util.TestDateFormatUtil.createExpireDate; class RunnerPostRepositoryReadTest extends RepositoryTestConfig { - @Autowired - private MemberRepository memberRepository; - - @Autowired - private RunnerRepository runnerRepository; - @Autowired private RunnerPostRepository runnerPostRepository; - @Autowired - private TagRepository tagRepository; - @Autowired private RunnerPostTagRepository runnerPostTagRepository; @Autowired - private EntityManager entityManager; + private EntityManager em; @DisplayName("RunnerPost 식별자로 RunnerPostTag 목록을 조회할 때 Tag 가 있으면 조회된다.") @Test void findRunnerPostTagsById_exist() { // given final Member ditoo = MemberFixture.createDitoo(); - entityManager.persist(ditoo); + em.persist(ditoo); final Runner runner = RunnerFixture.createRunner(ditoo); - entityManager.persist(runner); + em.persist(runner); final RunnerPost runnerPost = RunnerPostFixture.create(title("제 코드를 리뷰해주세요"), - contents("제 코드의 내용은 이렇습니다."), + implementedContents("제 코드의 내용은 이렇습니다."), + curiousContents("저는 이것이 궁금합니다."), + postscriptContents("잘 부탁드립니다."), pullRequestUrl("https://"), deadline(LocalDateTime.now().plusHours(10)), watchedCount(0), NOT_STARTED, + IsReviewed.notReviewed(), runner, null, - RunnerPostTagsFixture.runnerPostTags(new ArrayList<>())); + runnerPostTags(new ArrayList<>())); runnerPostRepository.save(runnerPost); final Tag java = TagFixture.createJava(); - entityManager.persist(java); + em.persist(java); final Tag spring = TagFixture.createSpring(); - entityManager.persist(spring); + em.persist(spring); final RunnerPostTag javaRunnerPostTag = RunnerPostTagFixture.create(runnerPost, java); final RunnerPostTag springRunnerPostTag = RunnerPostTagFixture.create(runnerPost, spring); runnerPost.addAllRunnerPostTags(List.of(javaRunnerPostTag, springRunnerPostTag)); + em.flush(); + em.close(); + // when final List expected = runnerPostTagRepository.joinTagByRunnerPostId(runnerPost.getId()); @@ -95,19 +97,25 @@ void findRunnerPostTagsById_exist() { void findByRunnerId() { // given final Member ditoo = MemberFixture.createDitoo(); - entityManager.persist(ditoo); + em.persist(ditoo); final Runner runner = RunnerFixture.createRunner(ditoo); - entityManager.persist(runner); + em.persist(runner); + + em.flush(); + em.close(); final RunnerPost runnerPost = RunnerPostFixture.create(title("제 코드를 리뷰해주세요"), - contents("제 코드의 내용은 이렇습니다."), + implementedContents("제 코드의 내용은 이렇습니다."), + curiousContents("저는 이것이 궁금합니다."), + postscriptContents("잘 부탁드립니다."), pullRequestUrl("https://"), deadline(LocalDateTime.now().plusHours(10)), watchedCount(0), NOT_STARTED, + IsReviewed.notReviewed(), runner, null, - RunnerPostTagsFixture.runnerPostTags(new ArrayList<>())); + runnerPostTags(new ArrayList<>())); runnerPostRepository.save(runnerPost); // when @@ -116,4 +124,60 @@ void findByRunnerId() { // then assertThat(actual).containsExactly(runnerPost); } + + @DisplayName("id 오름차순으로 게시글 페이징 조회한다") + @Test + void findByReviewStatus_sort_by_id() { + // given + final Member ditoo = MemberFixture.createDitoo(); + em.persist(ditoo); + final Runner runner = RunnerFixture.createRunner(ditoo); + em.persist(runner); + final Long runnerId = runner.getId(); + final LocalDateTime createdAt = createExpireDate(LocalDateTime.now()); + + insertRunnerPostByNativeQuery(1L, createdAt, runnerId); + insertRunnerPostByNativeQuery(3L, createdAt, runnerId); + insertRunnerPostByNativeQuery(2L, createdAt, runnerId); + + // when + final PageRequest pageable = PageRequest.of(0, 10, Sort.by(Sort.Order.desc("id"))); + + final Page actual = runnerPostRepository.findByReviewStatus(pageable, NOT_STARTED); + + // then + assertSoftly(softly -> { + softly.assertThat(actual.getTotalElements()).isEqualTo(3); + + List runnerPosts = actual.getContent(); + softly.assertThat(runnerPosts.get(0).getId()).isEqualTo(3L); + softly.assertThat(runnerPosts.get(1).getId()).isEqualTo(2L); + softly.assertThat(runnerPosts.get(2).getId()).isEqualTo(1L); + + softly.assertThat(runnerPosts.stream() + .filter(runnerPost -> runnerPost.getCreatedAt().isEqual(createdAt)) + .count()).isEqualTo(3); + }); + } + + private void insertRunnerPostByNativeQuery(final long value, final LocalDateTime createdAt, final Long runnerId) { + em.createNativeQuery(""" + insert into runner_post (id, title, implemented_contents, curious_contents, postscript_contents, + pull_request_url, deadline, review_status, created_at, updated_at, runner_id) + values (:id, :title, :implemented_contents, :curious_contents, :postscript_contents, + :pull_request_url, :deadline, :review_status, :created_at, :updated_at, :runner_id) + """) + .setParameter("id", value) + .setParameter("title", "제목") + .setParameter("implemented_contents", "구현 내용") + .setParameter("curious_contents", "궁금한 내용") + .setParameter("postscript_contents", "참고 사항") + .setParameter("pull_request_url", "pr url") + .setParameter("deadline", LocalDateTime.now().plusHours(100)) + .setParameter("review_status", NOT_STARTED.name()) + .setParameter("created_at", createdAt) + .setParameter("updated_at", createdAt) + .setParameter("runner_id", runnerId) + .executeUpdate(); + } } diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostReadServiceTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostReadServiceTest.java new file mode 100644 index 000000000..52d9fdd7e --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostReadServiceTest.java @@ -0,0 +1,154 @@ +package touch.baton.domain.runnerpost.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import touch.baton.config.ServiceTestConfig; +import touch.baton.domain.member.Member; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.repository.dto.ApplicantCountMappingDto; +import touch.baton.domain.runnerpost.vo.ReviewStatus; +import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.tag.Tag; +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 touch.baton.fixture.domain.TagFixture; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static touch.baton.fixture.vo.DeadlineFixture.deadline; +import static touch.baton.fixture.vo.TagNameFixture.tagName; + +class RunnerPostReadServiceTest extends ServiceTestConfig { + + private RunnerPostReadService runnerPostReadService; + + @BeforeEach + void setUp() { + runnerPostReadService = new RunnerPostReadService(runnerPostReadRepository); + } + + @DisplayName("러너 게시글을 태그 이름과 리뷰 상태를 조건으로 이용하여 페이징 조회에 성공한다.") + @Test + void readRunnerPostByTagNameAndReviewStatus() { + // given + final Member hyenaMember = memberRepository.save(MemberFixture.createHyena()); + final Runner hyenaRunner = runnerRepository.save(RunnerFixture.createRunner(hyenaMember)); + + final Tag javaTag = tagRepository.save(TagFixture.create(tagName("자바"))); + final Tag springTag = tagRepository.save(TagFixture.create(tagName("스프링"))); + + final RunnerPost expectedRunnerPostOne = runnerPostRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now().plusHours(100)), + List.of(javaTag, springTag) + )); + + final RunnerPost expectedRunnerPostTwo = runnerPostRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now().plusHours(100)), + List.of(javaTag) + )); + + runnerPostRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now().plusHours(100)), + List.of(springTag) + )); + + // when + final PageRequest pageOne = PageRequest.of(0, 10, Sort.by(Sort.Order.desc("id"))); + final Page actual = runnerPostReadService.readRunnerPostByTagNameAndReviewStatus( + pageOne, + javaTag.getTagName().getValue(), + ReviewStatus.NOT_STARTED + ); + + // then + assertSoftly(softly -> { + softly.assertThat(actual.isFirst()).isTrue(); + softly.assertThat(actual.isEmpty()).isFalse(); + softly.assertThat(actual.isLast()).isTrue(); + softly.assertThat(actual.getTotalPages()).isEqualTo(1); + softly.assertThat(actual.getTotalElements()).isEqualTo(2); + softly.assertThat(actual.getSize()).isEqualTo(10); + softly.assertThat(actual.getNumberOfElements()).isEqualTo(2); + softly.assertThat(actual.getNumber()).isEqualTo(0); + softly.assertThat(actual.getContent()).containsExactly(expectedRunnerPostTwo, expectedRunnerPostOne); + }); + } + + @DisplayName("러너 게시글 식별자값 목록으로 서포터 지원자 수 카운트(count)에 성공한다.") + @Test + void readApplicantCountsByRunnerPostIds() { + // given + final Member hyenaMember = memberRepository.save(MemberFixture.createHyena()); + final Runner hyenaRunner = runnerRepository.save(RunnerFixture.createRunner(hyenaMember)); + + final Member ditooMember = memberRepository.save(MemberFixture.createDitoo()); + final Supporter supporterDitoo = supporterRepository.save(SupporterFixture.create(ditooMember)); + + final Member ethanMember = memberRepository.save(MemberFixture.createEthan()); + final Supporter supporterEthan = supporterRepository.save(SupporterFixture.create(ethanMember)); + + final Member judyMember = memberRepository.save(MemberFixture.createJudy()); + final Supporter supporterJudy = supporterRepository.save(SupporterFixture.create(judyMember)); + + final Tag javaTag = tagRepository.save(TagFixture.create(tagName("자바"))); + final Tag springTag = tagRepository.save(TagFixture.create(tagName("스프링"))); + + final RunnerPost savedRunnerPostOne = runnerPostRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now().plusHours(100)), + List.of(javaTag, springTag) + )); + + final RunnerPost savedRunnerPostTwo = runnerPostRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now().plusHours(100)), + List.of(javaTag) + )); + + final RunnerPost savedRunnerPostThree = runnerPostRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now().plusHours(100)), + List.of(springTag) + )); + + supporterRunnerPostRepository.save(SupporterRunnerPostFixture.create(savedRunnerPostOne, supporterDitoo)); + supporterRunnerPostRepository.save(SupporterRunnerPostFixture.create(savedRunnerPostOne, supporterEthan)); + supporterRunnerPostRepository.save(SupporterRunnerPostFixture.create(savedRunnerPostOne, supporterJudy)); + + supporterRunnerPostRepository.save(SupporterRunnerPostFixture.create(savedRunnerPostThree, supporterDitoo)); + supporterRunnerPostRepository.save(SupporterRunnerPostFixture.create(savedRunnerPostThree, supporterEthan)); + + // when + final List runnerPostIds = List.of( + savedRunnerPostOne.getId(), + savedRunnerPostTwo.getId(), + savedRunnerPostThree.getId() + ); + + final ApplicantCountMappingDto actual = runnerPostReadService.readApplicantCountMappingByRunnerPostIds(runnerPostIds); + + // then + final ApplicantCountMappingDto expected = new ApplicantCountMappingDto(Map.of( + savedRunnerPostOne.getId(), 3L, + savedRunnerPostTwo.getId(), 0L, + savedRunnerPostThree.getId(), 2L + )); + + assertThat(actual).isEqualTo(expected); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceCreateTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceCreateTest.java index fe9b00599..f054bb57c 100644 --- a/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceCreateTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceCreateTest.java @@ -4,7 +4,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import touch.baton.config.ServiceTestConfig; -import touch.baton.domain.common.vo.Contents; import touch.baton.domain.common.vo.Title; import touch.baton.domain.common.vo.WatchedCount; import touch.baton.domain.member.Member; @@ -13,7 +12,10 @@ import touch.baton.domain.runnerpost.exception.RunnerPostBusinessException; import touch.baton.domain.runnerpost.service.dto.RunnerPostApplicantCreateRequest; import touch.baton.domain.runnerpost.service.dto.RunnerPostCreateRequest; +import touch.baton.domain.runnerpost.vo.CuriousContents; import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.ImplementedContents; +import touch.baton.domain.runnerpost.vo.PostscriptContents; import touch.baton.domain.runnerpost.vo.PullRequestUrl; import touch.baton.domain.supporter.Supporter; import touch.baton.domain.supporter.SupporterRunnerPost; @@ -27,7 +29,6 @@ import java.util.Optional; import static java.time.LocalDateTime.now; -import static java.time.LocalDateTime.of; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; @@ -41,7 +42,9 @@ class RunnerPostServiceCreateTest extends ServiceTestConfig { private static final String OTHER_TAG = "Spring"; private static final String PULL_REQUEST_URL = "https://github.com/cookienc"; private static final LocalDateTime DEADLINE = LocalDateTime.now().plusDays(10); - private static final String CONTENTS = "싸게 부탁드려요."; + private static final String IMPLEMENTED_CONTENTS = "이것 구현했어요."; + private static final String CURIOUS_CONTENTS = "궁금해."; + private static final String POSTSCRIPT_CONTENTS = "싸게 부탁드려요."; private RunnerPostService runnerPostService; @@ -64,7 +67,9 @@ void success() { List.of(TAG, OTHER_TAG), PULL_REQUEST_URL, DEADLINE, - CONTENTS); + IMPLEMENTED_CONTENTS, + CURIOUS_CONTENTS, + POSTSCRIPT_CONTENTS); final Member ethanMember = memberRepository.save(MemberFixture.createEthan()); final Runner runner = runnerRepository.save(RunnerFixture.createRunner(ethanMember)); @@ -78,7 +83,9 @@ void success() { final RunnerPost actual = maybeActual.get(); assertAll( () -> assertThat(actual.getTitle()).isEqualTo(new Title(TITLE)), - () -> assertThat(actual.getContents()).isEqualTo(new Contents(CONTENTS)), + () -> assertThat(actual.getImplementedContents()).isEqualTo(new ImplementedContents(IMPLEMENTED_CONTENTS)), + () -> assertThat(actual.getCuriousContents()).isEqualTo(new CuriousContents(CURIOUS_CONTENTS)), + () -> assertThat(actual.getPostscriptContents()).isEqualTo(new PostscriptContents(POSTSCRIPT_CONTENTS)), () -> assertThat(actual.getPullRequestUrl()).isEqualTo(new PullRequestUrl(PULL_REQUEST_URL)), () -> assertThat(actual.getDeadline()).isEqualTo(new Deadline(DEADLINE)), () -> assertThat(actual.getWatchedCount()).isEqualTo(new WatchedCount(0)), diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceReadTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceReadTest.java index ca9cbee4a..570f37f6d 100644 --- a/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceReadTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceReadTest.java @@ -6,7 +6,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import touch.baton.config.ServiceTestConfig; -import touch.baton.domain.common.vo.Contents; import touch.baton.domain.common.vo.TagName; import touch.baton.domain.common.vo.Title; import touch.baton.domain.common.vo.WatchedCount; @@ -20,13 +19,18 @@ import touch.baton.domain.runner.Runner; import touch.baton.domain.runnerpost.RunnerPost; import touch.baton.domain.runnerpost.exception.RunnerPostBusinessException; +import touch.baton.domain.runnerpost.vo.CuriousContents; import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.ImplementedContents; +import touch.baton.domain.runnerpost.vo.IsReviewed; +import touch.baton.domain.runnerpost.vo.PostscriptContents; 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.domain.tag.RunnerPostTags; import touch.baton.domain.tag.Tag; +import touch.baton.domain.tag.vo.TagReducedName; import touch.baton.fixture.domain.MemberFixture; import touch.baton.fixture.domain.RunnerFixture; import touch.baton.fixture.domain.RunnerPostFixture; @@ -43,6 +47,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.junit.jupiter.api.Assertions.assertAll; +import static touch.baton.domain.runnerpost.vo.ReviewStatus.IN_PROGRESS; import static touch.baton.domain.runnerpost.vo.ReviewStatus.NOT_STARTED; import static touch.baton.fixture.vo.DeadlineFixture.deadline; @@ -84,12 +89,15 @@ void success_findByRunnerPostId() { final LocalDateTime deadline = now(); final RunnerPost runnerPost = RunnerPost.builder() .title(new Title("제 코드 리뷰 좀 해주세요!!")) - .contents(new Contents("제 코드는 클린코드가 맞을까요?")) + .implementedContents(new ImplementedContents("제 코드는 클린코드가 맞을까요?")) + .curiousContents(new CuriousContents("궁금해요.")) + .postscriptContents(new PostscriptContents("잘 부탁드립니다.")) .deadline(new Deadline(deadline)) .pullRequestUrl(new PullRequestUrl("https://")) .watchedCount(new WatchedCount(0)) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) .reviewStatus(NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runner(runner) .supporter(null) .build(); @@ -97,6 +105,7 @@ void success_findByRunnerPostId() { final Tag tag = Tag.builder() .tagName(new TagName("자바")) + .tagReducedName(TagReducedName.from("자바")) .build(); tagRepository.save(tag); @@ -145,6 +154,30 @@ void success_findByRunnerId() { }); } + @DisplayName("ReviewStatus 로 RunnerPost 를 전체 조회한다.") + @Test + void success_readRunnerPostsByReviewStatus() { + // given + final Member memberDitoo = memberRepository.save(MemberFixture.createDitoo()); + final Runner runnerDitoo = runnerRepository.save(RunnerFixture.createRunner(memberDitoo)); + final Member memberJudy = memberRepository.save(MemberFixture.createJudy()); + final Supporter supporterJudy = supporterRepository.save(SupporterFixture.create(memberJudy)); + + final RunnerPost inProgressRunnerPost = RunnerPostFixture.create(runnerDitoo, deadline(now().plusHours(100))); + inProgressRunnerPost.assignSupporter(supporterJudy); + final RunnerPost savedInProgressRunnerPost = runnerPostRepository.save(inProgressRunnerPost); + + // when + final PageRequest pageable = PageRequest.of(0, 10); + final Page actualInProgressRunnerPosts = runnerPostService.readRunnerPostsByReviewStatus(pageable, IN_PROGRESS); + + // then + assertSoftly(softly -> { + softly.assertThat(actualInProgressRunnerPosts.getPageable()).isEqualTo(pageable); + softly.assertThat(actualInProgressRunnerPosts.getContent()).containsExactly(savedInProgressRunnerPost); + }); + } + @DisplayName("Supporter 외래키와 ReviewStatus 가 NOT_STARTED 가 아닌 것으로 러너 게시글을 조회한다.") @Test void readRunnerPostsBySupporterIdAndReviewStatusIsNot_NOT_STARTED() { @@ -164,7 +197,7 @@ void readRunnerPostsBySupporterIdAndReviewStatusIsNot_NOT_STARTED() { // when final PageRequest pageable = PageRequest.of(0, 10); final Page pageRunnerPosts - = runnerPostService.readRunnerPostsBySupporterIdAndReviewStatus(pageable, savedSupporterHyena.getId(), ReviewStatus.IN_PROGRESS); + = runnerPostService.readRunnerPostsBySupporterIdAndReviewStatus(pageable, savedSupporterHyena.getId(), IN_PROGRESS); // then assertAll( 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 736cf0f53..76ec6e56f 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 @@ -10,6 +10,7 @@ import touch.baton.domain.runnerpost.exception.RunnerPostBusinessException; import touch.baton.domain.runnerpost.exception.RunnerPostDomainException; import touch.baton.domain.runnerpost.service.dto.RunnerPostUpdateRequest; +import touch.baton.domain.runnerpost.vo.IsReviewed; import touch.baton.domain.runnerpost.vo.ReviewStatus; import touch.baton.domain.supporter.Supporter; import touch.baton.fixture.domain.MemberFixture; @@ -140,7 +141,8 @@ void fail_updateRunnerPostAppliedSupporter_if_is_not_owner_of_runnerPost() { @Test void updateRunnerPostReviewStatusDone() { // given - final RunnerPost targetRunnerPost = runnerPostRepository.save(RunnerPostFixture.createWithReviewStatus(runner, assignedSupporter, IN_PROGRESS)); + final IsReviewed isReviewed = IsReviewed.notReviewed(); + final RunnerPost targetRunnerPost = runnerPostRepository.save(RunnerPostFixture.createWithReviewStatus(runner, assignedSupporter, IN_PROGRESS, isReviewed)); // when runnerPostService.updateRunnerPostReviewStatusDone(targetRunnerPost.getId(), assignedSupporter); @@ -156,7 +158,8 @@ void updateRunnerPostReviewStatusDone() { @Test void fail_updateRunnerPostReviewStatusDone_if_invalid_runnerPostId() { // given - runnerPostRepository.save(RunnerPostFixture.createWithReviewStatus(runner, assignedSupporter, IN_PROGRESS)); + final IsReviewed isReviewed = IsReviewed.notReviewed(); + runnerPostRepository.save(RunnerPostFixture.createWithReviewStatus(runner, assignedSupporter, IN_PROGRESS, isReviewed)); final Long unsavedRunnerPostId = 100000L; // when, then @@ -168,7 +171,8 @@ void fail_updateRunnerPostReviewStatusDone_if_invalid_runnerPostId() { @Test void fail_updateRunnerPostReviewStatusDone_if_supporter_is_null() { // given - final RunnerPost targetRunnerPost = runnerPostRepository.save(RunnerPostFixture.createWithReviewStatus(runner, null, IN_PROGRESS)); + final IsReviewed isReviewed = IsReviewed.notReviewed(); + final RunnerPost targetRunnerPost = runnerPostRepository.save(RunnerPostFixture.createWithReviewStatus(runner, null, IN_PROGRESS, isReviewed)); // when, then assertThatThrownBy(() -> runnerPostService.updateRunnerPostReviewStatusDone(targetRunnerPost.getId(), assignedSupporter)) @@ -179,7 +183,8 @@ void fail_updateRunnerPostReviewStatusDone_if_supporter_is_null() { @Test void fail_updateRunnerPostReviewStatusDone_if_different_supporter_is_assigned() { // given - final RunnerPost targetRunnerPost = runnerPostRepository.save(RunnerPostFixture.createWithReviewStatus(runner, assignedSupporter, IN_PROGRESS)); + final IsReviewed isReviewed = IsReviewed.notReviewed(); + final RunnerPost targetRunnerPost = runnerPostRepository.save(RunnerPostFixture.createWithReviewStatus(runner, assignedSupporter, IN_PROGRESS, isReviewed)); final Member differentMember = memberRepository.save(MemberFixture.createHyena()); final Supporter differentSupporter = supporterRepository.save(SupporterFixture.create(differentMember)); @@ -192,7 +197,8 @@ void fail_updateRunnerPostReviewStatusDone_if_different_supporter_is_assigned() @Test void fail_updateRunnerPostReviewStatusDone_if_reviewStatus_is_overdue() { // given - final RunnerPost targetRunnerPost = runnerPostRepository.save(RunnerPostFixture.createWithReviewStatus(runner, assignedSupporter, OVERDUE)); + final IsReviewed isReviewed = IsReviewed.notReviewed(); + final RunnerPost targetRunnerPost = runnerPostRepository.save(RunnerPostFixture.createWithReviewStatus(runner, assignedSupporter, OVERDUE, isReviewed)); // when, then assertThatThrownBy(() -> runnerPostService.updateRunnerPostReviewStatusDone(targetRunnerPost.getId(), assignedSupporter)) diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostUpdateApplicantCancelationServiceTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostUpdateApplicantCancelationServiceTest.java index d0670f24b..ee3afe856 100644 --- a/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostUpdateApplicantCancelationServiceTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostUpdateApplicantCancelationServiceTest.java @@ -9,6 +9,7 @@ import touch.baton.domain.runnerpost.RunnerPost; import touch.baton.domain.runnerpost.exception.RunnerPostBusinessException; import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.IsReviewed; import touch.baton.domain.runnerpost.vo.ReviewStatus; import touch.baton.domain.supporter.Supporter; import touch.baton.domain.supporter.SupporterRunnerPost; @@ -84,12 +85,14 @@ void fail_when_runnerPost_not_found() { @Test void fail_when_runnerPost_reviewStatus_is_not_NOT_STARTED() { // given + final IsReviewed isReviewed = IsReviewed.notReviewed(); final RunnerPost runnerPost = runnerPostRepository.save( RunnerPostFixture.create( revieweeRunner, applicantSupporter, new Deadline(LocalDateTime.now().plusHours(100)), - ReviewStatus.IN_PROGRESS + ReviewStatus.IN_PROGRESS, + isReviewed )); final SupporterRunnerPost supporterRunnerPost = SupporterRunnerPostFixture.create(runnerPost, applicantSupporter); supporterRunnerPostRepository.save(supporterRunnerPost); diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/CuriousContentsTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/CuriousContentsTest.java new file mode 100644 index 000000000..d86174940 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/CuriousContentsTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.runnerpost.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class CuriousContentsTest { + + @DisplayName("value 가 null 이면 예외가 발생한다.") + @Test + void fail_if_value_is_null() { + assertThatThrownBy(() -> new CuriousContents(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/ImplementedContentsTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/ImplementedContentsTest.java new file mode 100644 index 000000000..b53e21ad0 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/ImplementedContentsTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.runnerpost.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ImplementedContentsTest { + + @DisplayName("value 가 null 이면 예외가 발생한다.") + @Test + void fail_if_value_is_null() { + assertThatThrownBy(() -> new ImplementedContents(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/IsReviewedTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/IsReviewedTest.java new file mode 100644 index 000000000..fb3183b21 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/IsReviewedTest.java @@ -0,0 +1,21 @@ +package touch.baton.domain.runnerpost.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class IsReviewedTest { + + @DisplayName("기본 생성의 default 값은 false 이다.") + @Test + void default_is_false() { + // given + final IsReviewed isReviewed = new IsReviewed(); + + // expect + final boolean actual = isReviewed.getValue(); + + assertThat(actual).isFalse(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/PostscriptContentsTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/PostscriptContentsTest.java new file mode 100644 index 000000000..216de4e35 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/PostscriptContentsTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.runnerpost.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PostscriptContentsTest { + + @DisplayName("value 가 null 이면 예외가 발생한다.") + @Test + void fail_if_value_is_null() { + assertThatThrownBy(() -> new PostscriptContents(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/supporter/SupporterFeedbackTest.java b/backend/baton/src/test/java/touch/baton/domain/supporter/SupporterFeedbackTest.java index e2d65347c..0e71329ef 100644 --- a/backend/baton/src/test/java/touch/baton/domain/supporter/SupporterFeedbackTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/supporter/SupporterFeedbackTest.java @@ -9,6 +9,7 @@ import touch.baton.domain.feedback.vo.ReviewType; import touch.baton.domain.runner.Runner; import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.vo.IsReviewed; import touch.baton.domain.supporter.vo.ReviewCount; import touch.baton.fixture.domain.MemberFixture; import touch.baton.fixture.domain.RunnerFixture; @@ -23,8 +24,10 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static touch.baton.domain.feedback.SupporterFeedback.builder; import static touch.baton.domain.runnerpost.vo.ReviewStatus.NOT_STARTED; -import static touch.baton.fixture.vo.ContentsFixture.contents; +import static touch.baton.fixture.vo.CuriousContentsFixture.curiousContents; import static touch.baton.fixture.vo.DeadlineFixture.deadline; +import static touch.baton.fixture.vo.ImplementedContentsFixture.implementedContents; +import static touch.baton.fixture.vo.PostscriptContentsFixture.postscriptContents; import static touch.baton.fixture.vo.PullRequestUrlFixture.pullRequestUrl; import static touch.baton.fixture.vo.TitleFixture.title; import static touch.baton.fixture.vo.WatchedCountFixture.watchedCount; @@ -48,11 +51,14 @@ void setUp() { runner = RunnerFixture.createRunner(MemberFixture.createDitoo()); runnerPost = RunnerPostFixture.create(title("제 코드를 리뷰해주세요"), - contents("제 코드의 내용은 이렇습니다."), + implementedContents("제 코드의 내용은 이렇습니다."), + curiousContents("제 궁금증은 이렇습니다."), + postscriptContents("제 참고 사항은 이렇습니다."), pullRequestUrl("https://"), deadline(LocalDateTime.now().plusHours(10)), watchedCount(0), NOT_STARTED, + IsReviewed.notReviewed(), runner, supporter, RunnerPostTagsFixture.runnerPostTags(new ArrayList<>())); diff --git a/backend/baton/src/test/java/touch/baton/domain/tag/RunnerPostTagTest.java b/backend/baton/src/test/java/touch/baton/domain/tag/RunnerPostTagTest.java index 7e61f7351..95eb857b2 100644 --- a/backend/baton/src/test/java/touch/baton/domain/tag/RunnerPostTagTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/tag/RunnerPostTagTest.java @@ -3,8 +3,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import touch.baton.domain.common.vo.Contents; -import touch.baton.domain.common.vo.TagName; import touch.baton.domain.common.vo.Title; import touch.baton.domain.common.vo.WatchedCount; import touch.baton.domain.member.Member; @@ -16,7 +14,11 @@ import touch.baton.domain.member.vo.SocialId; import touch.baton.domain.runner.Runner; import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.vo.CuriousContents; import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.ImplementedContents; +import touch.baton.domain.runnerpost.vo.IsReviewed; +import touch.baton.domain.runnerpost.vo.PostscriptContents; import touch.baton.domain.runnerpost.vo.PullRequestUrl; import touch.baton.domain.runnerpost.vo.ReviewStatus; import touch.baton.domain.supporter.Supporter; @@ -68,19 +70,20 @@ class Create { private final RunnerPost runnerPost = RunnerPost.builder() .title(new Title("JPA 정복")) - .contents(new Contents("김영한 짱짱맨")) + .implementedContents(new ImplementedContents("김영한 짱짱맨")) + .curiousContents(new CuriousContents("저는 클린코드가 궁금해요.")) + .postscriptContents(new PostscriptContents("저 상처 잘 받으니깐 부드럽게 말해주세요.")) .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) .deadline(new Deadline(LocalDateTime.now())) .watchedCount(new WatchedCount(0)) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runner(runner) .supporter(supporter) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) .build(); - private final Tag tag = Tag.builder() - .tagName(new TagName("자바")) - .build(); + private final Tag tag = Tag.newInstance("자바"); @DisplayName("성공한다.") @Test diff --git a/backend/baton/src/test/java/touch/baton/domain/tag/RunnerPostTagsTest.java b/backend/baton/src/test/java/touch/baton/domain/tag/RunnerPostTagsTest.java index 33a9bc852..e7010dba9 100644 --- a/backend/baton/src/test/java/touch/baton/domain/tag/RunnerPostTagsTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/tag/RunnerPostTagsTest.java @@ -38,7 +38,7 @@ void addAllRunnerPostTags() { .member(member) .runnerTechnicalTags(RunnerTechnicalTagsFixture.create(new ArrayList<>())) .build(); - final RunnerPost runnerpost = RunnerPost.newInstance("리뷰해주세요.", "제발요.", "https://github.com/cookienc", LocalDateTime.of(2099, 12, 12, 0, 0), runner); + final RunnerPost runnerpost = RunnerPost.newInstance("리뷰해주세요.", "제발요.", "디투가 궁금해요", "참고 해요~", "https://github.com/cookienc", LocalDateTime.of(2099, 12, 12, 0, 0), runner); final RunnerPostTag runnerPostTag = RunnerPostTag.builder() .runnerPost(runnerpost) diff --git a/backend/baton/src/test/java/touch/baton/domain/tag/TagTest.java b/backend/baton/src/test/java/touch/baton/domain/tag/TagTest.java index 609b43c71..558a8efd8 100644 --- a/backend/baton/src/test/java/touch/baton/domain/tag/TagTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/tag/TagTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; import touch.baton.domain.common.vo.TagName; import touch.baton.domain.tag.exception.TagDomainException; +import touch.baton.domain.tag.vo.TagReducedName; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -20,6 +21,7 @@ class Create { void success() { assertThatCode(() -> Tag.builder() .tagName(new TagName("자바")) + .tagReducedName(TagReducedName.from("자바")) .build() ).doesNotThrowAnyException(); } @@ -29,9 +31,19 @@ void success() { void fail_if_tagName_is_null() { assertThatThrownBy(() -> Tag.builder() .tagName(null) + .tagReducedName(TagReducedName.from("hello")) .build() - ).isInstanceOf(TagDomainException.class) - .hasMessage("Tag 의 tagName 은 null 일 수 없습니다."); + ).isInstanceOf(TagDomainException.class); + } + + @DisplayName("tag reduced name 이 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_tagReducedName_is_null() { + assertThatThrownBy(() -> Tag.builder() + .tagName(new TagName("hello")) + .tagReducedName(null) + .build() + ).isInstanceOf(TagDomainException.class); } } } diff --git a/backend/baton/src/test/java/touch/baton/domain/tag/repository/RunnerPostTagRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/tag/repository/RunnerPostTagRepositoryTest.java index 496eb7bd9..a99ecb85c 100644 --- a/backend/baton/src/test/java/touch/baton/domain/tag/repository/RunnerPostTagRepositoryTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/tag/repository/RunnerPostTagRepositoryTest.java @@ -4,7 +4,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import touch.baton.config.RepositoryTestConfig; -import touch.baton.domain.common.vo.Contents; import touch.baton.domain.common.vo.TagName; import touch.baton.domain.common.vo.Title; import touch.baton.domain.common.vo.WatchedCount; @@ -20,12 +19,17 @@ import touch.baton.domain.runner.repository.RunnerRepository; import touch.baton.domain.runnerpost.RunnerPost; import touch.baton.domain.runnerpost.repository.RunnerPostRepository; +import touch.baton.domain.runnerpost.vo.CuriousContents; import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.ImplementedContents; +import touch.baton.domain.runnerpost.vo.IsReviewed; +import touch.baton.domain.runnerpost.vo.PostscriptContents; import touch.baton.domain.runnerpost.vo.PullRequestUrl; import touch.baton.domain.runnerpost.vo.ReviewStatus; import touch.baton.domain.tag.RunnerPostTag; import touch.baton.domain.tag.RunnerPostTags; import touch.baton.domain.tag.Tag; +import touch.baton.domain.tag.vo.TagReducedName; import touch.baton.fixture.domain.RunnerTechnicalTagsFixture; import java.time.LocalDateTime; @@ -74,12 +78,15 @@ void success_joinTagByRunnerPostIds() { final LocalDateTime deadline = LocalDateTime.now(); final RunnerPost runnerPost = RunnerPost.builder() .title(new Title("제 코드 리뷰 좀 해주세요!!")) - .contents(new Contents("제 코드는 클린코드가 맞을까요?")) + .implementedContents(new ImplementedContents("제 코드는 클린코드가 맞을까요?")) + .curiousContents(new CuriousContents("저는 클린코드가 궁금해요.")) + .postscriptContents(new PostscriptContents("저 상처 잘 받으니깐 부드럽게 말해주세요.")) .deadline(new Deadline(deadline)) .pullRequestUrl(new PullRequestUrl("https://")) .watchedCount(new WatchedCount(1)) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runner(saveRunner) .supporter(null) .build(); @@ -87,6 +94,7 @@ void success_joinTagByRunnerPostIds() { final Tag tag = Tag.builder() .tagName(new TagName("자바")) + .tagReducedName(TagReducedName.from("자바")) .build(); tagRepository.save(tag); @@ -128,12 +136,15 @@ void success_joinTagByRunnerPostIds_if_tag_is_empty() { final LocalDateTime deadline = LocalDateTime.now(); final RunnerPost runnerPost = RunnerPost.builder() .title(new Title("제 코드 리뷰 좀 해주세요!!")) - .contents(new Contents("제 코드는 클린코드가 맞을까요?")) + .implementedContents(new ImplementedContents("제 코드는 클린코드가 맞을까요?")) + .curiousContents(new CuriousContents("저는 클린코드가 궁금해요.")) + .postscriptContents(new PostscriptContents("저 상처 잘 받으니깐 부드럽게 말해주세요.")) .deadline(new Deadline(deadline)) .pullRequestUrl(new PullRequestUrl("https://")) .watchedCount(new WatchedCount(1)) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runner(saveRunner) .supporter(null) .build(); diff --git a/backend/baton/src/test/java/touch/baton/domain/tag/repository/TagRepositoryReadTest.java b/backend/baton/src/test/java/touch/baton/domain/tag/repository/TagRepositoryReadTest.java new file mode 100644 index 000000000..d3bcc18cb --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/tag/repository/TagRepositoryReadTest.java @@ -0,0 +1,116 @@ +package touch.baton.domain.tag.repository; + +import jakarta.persistence.EntityManager; +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.common.vo.TagName; +import touch.baton.domain.tag.Tag; +import touch.baton.domain.tag.vo.TagReducedName; +import touch.baton.fixture.domain.TagFixture; +import touch.baton.fixture.vo.TagNameFixture; + +import java.util.List; +import java.util.Optional; +import java.util.stream.IntStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class TagRepositoryReadTest extends RepositoryTestConfig { + + @Autowired + private TagRepository tagRepository; + + @Autowired + private EntityManager em; + + @DisplayName("이름으로 단건 검색한다.") + @Test + void findByName() { + // given + final Tag javaTag = persistTag("java"); + final Tag uppercaseJavaTag = persistTag("Java"); + + em.flush(); + em.close(); + + // when + final Optional actual = tagRepository.findByTagName(TagNameFixture.tagName("java")); + + // then + assertThat(actual).contains(javaTag); + } + + @DisplayName("이름을 오름차순으로 10개 검색한다.") + @Test + void success_readTagsByReducedName() { + // given + persistTag("ja va"); + persistTag("j ava1"); + persistTag("ja va2"); + persistTag("jav a3"); + persistTag("java 4"); + persistTag("ja va5"); + persistTag("j ava6"); + persistTag("ja va7"); + persistTag("jav a8"); + persistTag("java 9"); + persistTag("assert ja"); + + em.flush(); + em.close(); + + // when + final TagReducedName reducedName = TagReducedName.from("ja"); + final List actual = tagRepository.readTagsByReducedName(reducedName); + + // then + assertSoftly(softly -> { + softly.assertThat(actual).hasSize(10); + softly.assertThat(actual.get(0).getTagName().getValue()).isEqualTo("ja va"); + softly.assertThat(actual.get(9).getTagName().getValue()).isEqualTo("java 9"); + }); + + } + + @DisplayName("이름으로 시작하는 태그만 검색한다") + @Test + void fail_readTagsByReducedName() { + // given + final Tag tag = TagFixture.create(TagNameFixture.tagName("assertj")); + final TagReducedName reducedName = TagReducedName.from("j"); + tagRepository.save(tag); + + // when + final List actual = tagRepository.readTagsByReducedName(reducedName); + + // then + assertThat(actual.isEmpty()).isTrue(); + } + + @DisplayName("이름에 해당하는 태그가 없다면 빈 배열을 반환한다.") + @Test + void success_readTagsByReducedName_when_no_match_tag() { + // given + final TagReducedName reducedName = TagReducedName.from("hi"); + persistTag("hellohi"); + + em.flush(); + em.close(); + + // when + final List actual = tagRepository.readTagsByReducedName(reducedName); + + // then + assertThat(actual.isEmpty()).isTrue(); + } + + private Tag persistTag(final String tagName) { + final Tag tag = TagFixture.create(TagNameFixture.tagName(tagName)); + em.persist(tag); + + return tag; + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/tag/service/TagServiceReadTest.java b/backend/baton/src/test/java/touch/baton/domain/tag/service/TagServiceReadTest.java new file mode 100644 index 000000000..45b957299 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/tag/service/TagServiceReadTest.java @@ -0,0 +1,51 @@ +package touch.baton.domain.tag.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import touch.baton.config.ServiceTestConfig; +import touch.baton.domain.tag.Tag; +import touch.baton.fixture.domain.TagFixture; +import touch.baton.fixture.vo.TagNameFixture; + +import java.util.List; +import java.util.stream.IntStream; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class TagServiceReadTest extends ServiceTestConfig { + + private TagService tagService; + + @BeforeEach + void setUp() { + tagService = new TagService(tagRepository); + } + + @DisplayName("Tag의 이름으로 Tag를 오름차순으로 10개 조회한다.") + @Test + void success_readTagsByReducedName() { + // given + tagRepository.save(TagFixture.create(TagNameFixture.tagName("ja va"))); + tagRepository.save(TagFixture.create(TagNameFixture.tagName("jav a1"))); + tagRepository.save(TagFixture.create(TagNameFixture.tagName("j ava2"))); + tagRepository.save(TagFixture.create(TagNameFixture.tagName("ja va3"))); + tagRepository.save(TagFixture.create(TagNameFixture.tagName("jav a4"))); + tagRepository.save(TagFixture.create(TagNameFixture.tagName("java 5"))); + tagRepository.save(TagFixture.create(TagNameFixture.tagName("j ava6"))); + tagRepository.save(TagFixture.create(TagNameFixture.tagName("ja va7"))); + tagRepository.save(TagFixture.create(TagNameFixture.tagName("jav a8"))); + tagRepository.save(TagFixture.create(TagNameFixture.tagName("java 9"))); + tagRepository.save(TagFixture.create(TagNameFixture.tagName("ju ja"))); + + // when + final List actual = tagService.readTagsByReducedName("j a"); + + // then + assertSoftly(softly -> { + softly.assertThat(actual).hasSize(10); + softly.assertThat(actual.get(0).getTagName().getValue()).isEqualTo("ja va"); + softly.assertThat(actual.get(9).getTagName().getValue()).isEqualTo("java 9"); + }); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/tag/vo/TagReducedNameTest.java b/backend/baton/src/test/java/touch/baton/domain/tag/vo/TagReducedNameTest.java new file mode 100644 index 000000000..9cf64d9ca --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/tag/vo/TagReducedNameTest.java @@ -0,0 +1,45 @@ +package touch.baton.domain.tag.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class TagReducedNameTest { + + @DisplayName("생성자의 매개변수로 들어온 notReducedValue 가 null 이면 예외가 발생한다.") + @Test + void construct_fail_if_value_is_null() { + assertThatThrownBy(() -> TagReducedName.from(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("정적 팩토리 메소드로 생성 시에 value 의 공백은 제거된다.") + @Test + void reduce_blank_when_construct() { + // given + final String notReducedValue = "d i t o o"; + final String expected = "ditoo"; + + // when + final TagReducedName tagReducedName = TagReducedName.from(notReducedValue); + + // then + assertThat(tagReducedName.getValue()).isEqualTo(expected); + } + + @DisplayName("정적 팩토리 메소드로 생성 시에 value 는 모두 소문자로 변한다.") + @Test + void value_change_to_lower_case_when_construct() { + // given + final String notReducedValue = "DiToO"; + final String expected = "ditoo"; + + // when + final TagReducedName tagReducedName = TagReducedName.from(notReducedValue); + + // then + assertThat(tagReducedName.getValue()).isEqualTo(expected); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/technicaltag/repository/SupporterTechnicalTagRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/technicaltag/repository/SupporterTechnicalTagRepositoryTest.java index d76612dbb..6f9d21652 100644 --- a/backend/baton/src/test/java/touch/baton/domain/technicaltag/repository/SupporterTechnicalTagRepositoryTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/technicaltag/repository/SupporterTechnicalTagRepositoryTest.java @@ -24,28 +24,29 @@ class SupporterTechnicalTagRepositoryTest extends RepositoryTestConfig { private SupporterTechnicalTagRepository supporterTechnicalTagRepository; @Autowired - private EntityManager entityManager; + private EntityManager em; @DisplayName("batch 로 supporter 의 모든 SupporterTechnicalTag 를 삭제한다.") @Test void deleteBySupporter() { // given final Member member = MemberFixture.createDitoo(); - entityManager.persist(member); + em.persist(member); final Supporter supporter = SupporterFixture.create(member); - entityManager.persist(supporter); + em.persist(supporter); final TechnicalTag technicalTag1 = TechnicalTagFixture.createReact(); final TechnicalTag technicalTag2 = TechnicalTagFixture.createSpring(); final TechnicalTag technicalTag3 = TechnicalTagFixture.createJava(); - entityManager.persist(technicalTag1); - entityManager.persist(technicalTag2); - entityManager.persist(technicalTag3); + em.persist(technicalTag1); + em.persist(technicalTag2); + em.persist(technicalTag3); final SupporterTechnicalTag supporterTechnicalTag1 = SupporterTechnicalTagFixture.create(supporter, technicalTag1); final SupporterTechnicalTag supporterTechnicalTag2 = SupporterTechnicalTagFixture.create(supporter, technicalTag2); final SupporterTechnicalTag supporterTechnicalTag3 = SupporterTechnicalTagFixture.create(supporter, technicalTag3); final List savedSupporterTechnicalTags = List.of(supporterTechnicalTag1, supporterTechnicalTag2, supporterTechnicalTag3); supporterTechnicalTagRepository.saveAll(savedSupporterTechnicalTags); - entityManager.flush(); + em.flush(); + em.close(); // when final int expected = savedSupporterTechnicalTags.size(); diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/RefreshTokenFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/RefreshTokenFixture.java new file mode 100644 index 000000000..466b815fe --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/RefreshTokenFixture.java @@ -0,0 +1,20 @@ +package touch.baton.fixture.domain; + +import touch.baton.domain.member.Member; +import touch.baton.domain.oauth.token.ExpireDate; +import touch.baton.domain.oauth.token.RefreshToken; +import touch.baton.domain.oauth.token.Token; + +public abstract class RefreshTokenFixture { + + private RefreshTokenFixture() { + } + + public static RefreshToken create(final Member member, final Token refreshToken, final ExpireDate expireDate) { + return RefreshToken.builder() + .member(member) + .token(refreshToken) + .expireDate(expireDate) + .build(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerPostFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerPostFixture.java index 6b17699e5..bae8fc7c1 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerPostFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerPostFixture.java @@ -1,11 +1,14 @@ package touch.baton.fixture.domain; -import touch.baton.domain.common.vo.Contents; import touch.baton.domain.common.vo.Title; import touch.baton.domain.common.vo.WatchedCount; import touch.baton.domain.runner.Runner; import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.vo.CuriousContents; import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.ImplementedContents; +import touch.baton.domain.runnerpost.vo.IsReviewed; +import touch.baton.domain.runnerpost.vo.PostscriptContents; import touch.baton.domain.runnerpost.vo.PullRequestUrl; import touch.baton.domain.runnerpost.vo.ReviewStatus; import touch.baton.domain.supporter.Supporter; @@ -24,22 +27,28 @@ private RunnerPostFixture() { } public static RunnerPost create(final Title title, - final Contents contents, + final ImplementedContents implementedContents, + final CuriousContents curiousContents, + final PostscriptContents postscriptContents, final PullRequestUrl pullRequestUrl, final Deadline deadline, final WatchedCount watchedCount, final ReviewStatus reviewStatus, + final IsReviewed isReviewed, final Runner runner, final Supporter supporter, final RunnerPostTags runnerPostTags ) { return RunnerPost.builder() .title(title) - .contents(contents) + .implementedContents(implementedContents) + .curiousContents(curiousContents) + .postscriptContents(postscriptContents) .pullRequestUrl(pullRequestUrl) .deadline(deadline) .watchedCount(watchedCount) .reviewStatus(reviewStatus) + .isReviewed(isReviewed) .runner(runner) .supporter(supporter) .runnerPostTags(runnerPostTags) @@ -49,25 +58,35 @@ public static RunnerPost create(final Title title, public static RunnerPost create(final Runner runner, final Deadline deadline) { return RunnerPost.builder() .title(new Title("테스트 제목")) - .contents(new Contents("테스트 내용")) + .implementedContents(new ImplementedContents("테스트 내용")) + .curiousContents(new CuriousContents("테스트 궁금 점")) + .postscriptContents(new PostscriptContents("테스트 참고 사항")) .pullRequestUrl(new PullRequestUrl("https://테스트")) .deadline(deadline) .watchedCount(new WatchedCount(0)) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runner(runner) .supporter(null) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) .build(); } - public static RunnerPost create(final Runner runner, final Deadline deadline, final ReviewStatus reviewStatus) { + public static RunnerPost create(final Runner runner, + final Deadline deadline, + final ReviewStatus reviewStatus, + final IsReviewed isReviewed + ) { return RunnerPost.builder() .title(new Title("테스트 제목")) - .contents(new Contents("테스트 내용")) + .implementedContents(new ImplementedContents("테스트 내용")) + .curiousContents(new CuriousContents("테스트 궁금 점")) + .postscriptContents(new PostscriptContents("테스트 참고 사항")) .pullRequestUrl(new PullRequestUrl("https://테스트")) .deadline(deadline) .watchedCount(new WatchedCount(0)) .reviewStatus(reviewStatus) + .isReviewed(isReviewed) .runner(runner) .supporter(null) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) @@ -77,11 +96,14 @@ public static RunnerPost create(final Runner runner, final Deadline deadline, fi public static RunnerPost create(final Runner runner, final Deadline deadline, List tags) { final RunnerPost runnerPost = RunnerPost.builder() .title(new Title("테스트 제목")) - .contents(new Contents("테스트 내용")) + .implementedContents(new ImplementedContents("테스트 내용")) + .curiousContents(new CuriousContents("테스트 궁금 점")) + .postscriptContents(new PostscriptContents("테스트 참고 사항")) .pullRequestUrl(new PullRequestUrl("https://테스트")) .deadline(deadline) .watchedCount(new WatchedCount(0)) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runner(runner) .supporter(null) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) @@ -96,44 +118,40 @@ public static RunnerPost create(final Runner runner, final Deadline deadline, Li return runnerPost; } - - public static RunnerPost create(final Runner runner, final RunnerPostTags runnerPostTags, final Deadline deadline) { - return RunnerPost.builder() - .title(new Title("테스트 제목")) - .contents(new Contents("테스트 내용")) - .pullRequestUrl(new PullRequestUrl("https://테스트")) - .deadline(deadline) - .watchedCount(new WatchedCount(0)) - .runner(runner) - .supporter(null) - .runnerPostTags(runnerPostTags) - .build(); - } - public static RunnerPost create(final Runner runner, final Supporter supporter) { return RunnerPost.builder() .title(new Title("테스트 제목")) - .contents(new Contents("테스트 내용")) + .implementedContents(new ImplementedContents("테스트 내용")) + .curiousContents(new CuriousContents("테스트 궁금 점")) + .postscriptContents(new PostscriptContents("테스트 참고 사항")) .pullRequestUrl(new PullRequestUrl("https://테스트")) .deadline(DeadlineFixture.deadline(LocalDateTime.now().plusHours(100))) .watchedCount(new WatchedCount(0)) .runner(runner) .supporter(supporter) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) .build(); } - public static RunnerPost createWithReviewStatus(final Runner runner, final Supporter supporter, final ReviewStatus reviewStatus) { + public static RunnerPost createWithReviewStatus(final Runner runner, + final Supporter supporter, + final ReviewStatus reviewStatus, + final IsReviewed isReviewed + ) { return RunnerPost.builder() .title(new Title("테스트 제목")) - .contents(new Contents("테스트 내용")) + .implementedContents(new ImplementedContents("테스트 내용")) + .curiousContents(new CuriousContents("테스트 궁금 점")) + .postscriptContents(new PostscriptContents("테스트 참고 사항")) .pullRequestUrl(new PullRequestUrl("https://테스트")) .deadline(DeadlineFixture.deadline(LocalDateTime.now().plusHours(100))) .watchedCount(new WatchedCount(0)) .runner(runner) .supporter(supporter) .reviewStatus(reviewStatus) + .isReviewed(isReviewed) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) .build(); } @@ -141,13 +159,16 @@ public static RunnerPost createWithReviewStatus(final Runner runner, final Suppo public static RunnerPost create(final Runner runner, final Supporter supporter, final Deadline deadline) { return RunnerPost.builder() .title(new Title("테스트 제목")) - .contents(new Contents("테스트 내용")) + .implementedContents(new ImplementedContents("테스트 내용")) + .curiousContents(new CuriousContents("테스트 궁금 점")) + .postscriptContents(new PostscriptContents("테스트 참고 사항")) .pullRequestUrl(new PullRequestUrl("https://테스트")) .deadline(deadline) .watchedCount(new WatchedCount(0)) .runner(runner) .supporter(supporter) .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) .build(); } @@ -155,31 +176,22 @@ public static RunnerPost create(final Runner runner, final Supporter supporter, public static RunnerPost create(final Runner runner, final Supporter supporter, final Deadline deadline, - final ReviewStatus reviewStatus + final ReviewStatus reviewStatus, + final IsReviewed isReviewed ) { return RunnerPost.builder() .title(new Title("테스트 제목")) - .contents(new Contents("테스트 내용")) + .implementedContents(new ImplementedContents("테스트 내용")) + .curiousContents(new CuriousContents("테스트 궁금 점")) + .postscriptContents(new PostscriptContents("테스트 참고 사항")) .pullRequestUrl(new PullRequestUrl("https://테스트")) .deadline(deadline) .watchedCount(new WatchedCount(0)) .runner(runner) .supporter(supporter) .reviewStatus(reviewStatus) + .isReviewed(isReviewed) .runnerPostTags(new RunnerPostTags(new ArrayList<>())) .build(); } - - public static RunnerPost create(final Runner runner, final Supporter supporter, final RunnerPostTags runnerPostTags, final Deadline deadline) { - return RunnerPost.builder() - .title(new Title("테스트 제목")) - .contents(new Contents("테스트 내용")) - .pullRequestUrl(new PullRequestUrl("https://테스트")) - .deadline(deadline) - .watchedCount(new WatchedCount(0)) - .runner(runner) - .supporter(supporter) - .runnerPostTags(runnerPostTags) - .build(); - } } diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/TagFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/TagFixture.java index 6c40b717a..2b659e111 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/domain/TagFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/TagFixture.java @@ -2,6 +2,7 @@ import touch.baton.domain.common.vo.TagName; import touch.baton.domain.tag.Tag; +import touch.baton.domain.tag.vo.TagReducedName; import touch.baton.fixture.vo.TagNameFixture; public abstract class TagFixture { @@ -12,6 +13,7 @@ private TagFixture() { public static Tag create(final TagName tagName) { return Tag.builder() .tagName(tagName) + .tagReducedName(TagReducedName.from(tagName.getValue())) .build(); } diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/AuthorizationHeaderFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/AuthorizationHeaderFixture.java new file mode 100644 index 000000000..10c443506 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/AuthorizationHeaderFixture.java @@ -0,0 +1,19 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.oauth.AuthorizationHeader; + +public class AuthorizationHeaderFixture { + + private static final String BEARER = "Bearer "; + + private AuthorizationHeaderFixture() { + } + + public static AuthorizationHeader authorizationHeader(final String authorizationHeader) { + return new AuthorizationHeader(authorizationHeader); + } + + public static AuthorizationHeader bearerAuthorizationHeader(final String authorizationHeader) { + return new AuthorizationHeader(BEARER + authorizationHeader); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/CuriousContentsFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/CuriousContentsFixture.java new file mode 100644 index 000000000..768f50574 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/CuriousContentsFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.runnerpost.vo.CuriousContents; + +public abstract class CuriousContentsFixture { + + private CuriousContentsFixture() { + } + + public static CuriousContents curiousContents(final String value) { + return new CuriousContents(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/DescriptionFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/DescriptionFixture.java index c2a2fa263..9354b8828 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/vo/DescriptionFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/DescriptionFixture.java @@ -2,7 +2,7 @@ import touch.baton.domain.feedback.vo.Description; -public class DescriptionFixture { +public abstract class DescriptionFixture { private DescriptionFixture() { } diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/ExpireDateFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/ExpireDateFixture.java new file mode 100644 index 000000000..1d923d90f --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/ExpireDateFixture.java @@ -0,0 +1,15 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.oauth.token.ExpireDate; + +import java.time.LocalDateTime; + +public abstract class ExpireDateFixture { + + private ExpireDateFixture() { + } + + public static ExpireDate expireDate(final LocalDateTime expireDate) { + return new ExpireDate(expireDate); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/ImplementedContentsFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/ImplementedContentsFixture.java new file mode 100644 index 000000000..468933ee3 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/ImplementedContentsFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.runnerpost.vo.ImplementedContents; + +public abstract class ImplementedContentsFixture { + + private ImplementedContentsFixture() { + } + + public static ImplementedContents implementedContents(final String value) { + return new ImplementedContents(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/PostscriptContentsFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/PostscriptContentsFixture.java new file mode 100644 index 000000000..c03fe92fd --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/PostscriptContentsFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.runnerpost.vo.PostscriptContents; + +public abstract class PostscriptContentsFixture { + + private PostscriptContentsFixture() { + } + + public static PostscriptContents postscriptContents(final String value) { + return new PostscriptContents(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/TokenFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/TokenFixture.java new file mode 100644 index 000000000..1ca72494d --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/TokenFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.oauth.token.Token; + +public abstract class TokenFixture { + + private TokenFixture() { + } + + public static Token token(final String value) { + return new Token(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/infra/auth/jwt/JwtEncoderAndDecoderTest.java b/backend/baton/src/test/java/touch/baton/infra/auth/jwt/JwtEncoderAndDecoderTest.java index 6418f8a76..664913a32 100644 --- a/backend/baton/src/test/java/touch/baton/infra/auth/jwt/JwtEncoderAndDecoderTest.java +++ b/backend/baton/src/test/java/touch/baton/infra/auth/jwt/JwtEncoderAndDecoderTest.java @@ -12,6 +12,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static touch.baton.fixture.vo.AuthorizationHeaderFixture.bearerAuthorizationHeader; class JwtEncoderAndDecoderTest { @@ -25,7 +26,7 @@ class JwtEncoderAndDecoderTest { @BeforeEach void setUp() { - this.jwtConfig = new JwtConfig("hyenahyenahyenahyenahyenahyenahyenahyenahyenahyenahyenahyena", "hyena"); + this.jwtConfig = new JwtConfig("hyenahyenahyenahyenahyenahyenahyenahyenahyenahyenahyenahyena", "hyena", 30); this.jwtDecoder = new JwtDecoder(this.jwtConfig); this.jwtEncoder = new JwtEncoder(this.jwtConfig); } @@ -37,7 +38,7 @@ void encode_and_decode() { final String encodedJwt = jwtEncoder.jwtToken(Map.of("socialId", "testSocialId")); // when - final Claims claims = jwtDecoder.parseJwtToken(encodedJwt); + final Claims claims = jwtDecoder.parseAuthorizationHeader(bearerAuthorizationHeader(encodedJwt)); final String socialId = claims.get("socialId", String.class); // then @@ -51,11 +52,24 @@ void fail_decode_with_wrong_secretKey() { final String encodedJwt = jwtEncoder.jwtToken(Map.of("socialId", "testSocialId")); // when - final JwtConfig wrongJwtConfig = new JwtConfig("wrongSecretKeywrongSecretKeywrongSecretKey", "hyena"); + final JwtConfig wrongJwtConfig = new JwtConfig("wrongSecretKeywrongSecretKeywrongSecretKey", "hyena", 30); final JwtDecoder wrongJwtDecoder = new JwtDecoder(wrongJwtConfig); // then - assertThatThrownBy(() -> wrongJwtDecoder.parseJwtToken(encodedJwt)) + assertThatThrownBy(() -> wrongJwtDecoder.parseAuthorizationHeader(bearerAuthorizationHeader(encodedJwt))) + .isInstanceOf(OauthRequestException.class); + } + + @DisplayName("exp가 만료된 jwt 를 디코드할 때 예외가 발생한다") + @Test + void fail_decode_when_exp_is_already_expired() { + // given + final JwtConfig expiredJwtConfig = new JwtConfig("hyenahyenahyenahyenahyenahyenahyenahyenahyenahyenahyenahyena", "hyena", -1); + final JwtEncoder expiredJwtEncoder = new JwtEncoder(expiredJwtConfig); + final String encodedJwt = expiredJwtEncoder.jwtToken(Map.of("socialId", "testSocialId")); + + // when, then + assertThatThrownBy(() -> jwtDecoder.parseAuthorizationHeader(bearerAuthorizationHeader(encodedJwt))) .isInstanceOf(OauthRequestException.class); } } diff --git a/backend/baton/src/test/java/touch/baton/util/TestDateFormatUtil.java b/backend/baton/src/test/java/touch/baton/util/TestDateFormatUtil.java new file mode 100644 index 000000000..efc43bbe5 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/util/TestDateFormatUtil.java @@ -0,0 +1,14 @@ +package touch.baton.util; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class TestDateFormatUtil { + + public static LocalDateTime createExpireDate(final LocalDateTime expireDate) { + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS"); + final String formattedExpireDate = expireDate.format(formatter); + + return LocalDateTime.parse(formattedExpireDate, formatter); + } +} diff --git a/backend/baton/src/test/resources/application.yml b/backend/baton/src/test/resources/application.yml new file mode 100644 index 000000000..311a5a907 --- /dev/null +++ b/backend/baton/src/test/resources/application.yml @@ -0,0 +1,38 @@ +spring: + flyway: + enabled: false + + jpa: + properties: + hibernate: + format_sql: true + hibernate: + ddl-auto: create + + data: + web: + pageable: + default-page-size: 10 + one-indexed-parameters: true + +logging: + config: classpath:logs/log4j2.xml + level: + org.hibernate.SQL: debug + org.hibernate.orm.jdbc.bind: trace + +refresh_token: + expire_minutes: 30 + +oauth: + github: + client_id: ${OAUTH_GITHUB_CLIENT_ID} + redirect_uri: ${OAUTH_GITHUB_REDIRECT_URI} + client_secret: ${OAUTH_GITHUB_CLIENT_SECRET} + scope: ${OAUTH_GITHUB_SCOPE} + +github: + personal_access_token: test + +cors: + allowed-origin: http://localhost:3000