From bb2cb10ec2d6c8f6e487204cf574c714cfcb938f Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 13 Oct 2023 18:23:13 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B0=B1=EC=97=94=EB=93=9C=20=EB=B3=91?= =?UTF-8?q?=ED=95=A9=20=EC=9E=91=EC=97=85=20(#670)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-be-ci-cd-push.yml | 8 +- .github/workflows/dev-be-ci-cd-push.yml | 9 +- backend/baton/.gitignore | 1 + backend/baton/build.gradle | 6 + .../src/docs/asciidoc/GithubOauthApi.adoc | 16 +- .../docs/asciidoc/NotificationDeleteApi.adoc | 29 + .../asciidoc/NotificationLoginReadApi.adoc | 29 + .../docs/asciidoc/NotificationUpdateApi.adoc | 29 + .../src/docs/asciidoc/OauthLogoutApi.adoc | 25 + .../src/docs/asciidoc/RunnerPostReadApi.adoc | 92 +- .../src/docs/asciidoc/RunnerReadApi.adoc | 6 +- backend/baton/src/docs/asciidoc/index.adoc | 15 + .../ScheduleRunnerPostRepository.java | 2 +- .../baton/config/ArgumentResolverConfig.java | 6 +- .../touch/baton/config/QuerydslConfig.java | 19 + .../java/touch/baton/config/WebMvcConfig.java | 7 +- .../config/converter/OauthTypeConverter.java | 2 +- .../converter/ReviewStatusConverter.java | 2 +- .../baton/config/filter/FilterConfig.java | 2 +- .../baton/config/filter/MDCLoggingFilter.java | 7 +- .../domain/common/TruncatedBaseEntity.java | 22 + .../common/exception/ClientErrorCode.java | 1 + .../domain/common/request/PageParams.java | 14 + .../domain/common/response/IdExtractable.java | 6 + .../domain/common/response/PageResponse.java | 53 +- .../feedback/command/SupporterFeedback.java | 100 ++ .../controller/FeedbackCommandController.java | 38 + .../SupporterFeedbackCommandRepository.java | 9 + .../service/FeedbackCommandService.java | 56 ++ .../dto/SupporterFeedBackCreateRequest.java | 16 + .../feedback/command/vo/Description.java | 32 + .../feedback/command/vo/ReviewType.java | 6 + .../baton/domain/member/command/Member.java | 140 +++ .../baton/domain/member/command/Runner.java | 108 +++ .../domain/member/command/Supporter.java | 135 +++ .../member/command/SupporterRunnerPost.java | 92 ++ .../controller/MemberBranchController.java | 33 + .../controller/RunnerCommandController.java | 32 + .../SupporterCommandController.java | 32 + .../response/LoginMemberInfoResponse.java | 10 + .../repository/MemberCommandRepository.java | 7 + .../SupporterCommandRepository.java | 7 + .../SupporterRunnerPostCommandRepository.java | 13 + .../service/GithubBranchManageable.java | 6 + .../command/service/RunnerCommandService.java | 66 ++ .../service/SupporterCommandService.java | 53 ++ .../service/dto/GithubRepoNameRequest.java | 7 + .../service/dto/RunnerUpdateRequest.java | 17 + .../service/dto/SupporterUpdateRequest.java | 18 + .../domain/member/command/vo/Company.java | 32 + .../domain/member/command/vo/GithubUrl.java | 32 + .../domain/member/command/vo/ImageUrl.java | 32 + .../member/command/vo/Introduction.java | 38 + .../domain/member/command/vo/MemberName.java | 30 + .../domain/member/command/vo/Message.java | 32 + .../domain/member/command/vo/OauthId.java | 32 + .../domain/member/command/vo/ReviewCount.java | 27 + .../domain/member/command/vo/SocialId.java | 32 + .../exception/RunnerBusinessException.java | 10 + .../exception/RunnerDomainException.java | 10 + .../exception/RunnerRequestException.java | 11 + .../exception/SupporterBusinessException.java | 10 + .../exception/SupporterDomainException.java | 10 + .../exception/SupporterRequestException.java | 11 + .../controller/MemberQueryController.java | 21 + .../controller/RunnerQueryController.java | 33 + .../controller/SupporterQueryController.java | 35 + .../controller/response/RunnerResponse.java | 86 ++ .../response/SupporterResponse.java | 79 ++ .../repository/RunnerQueryRepository.java | 19 + .../repository/SupporterQueryRepository.java | 19 + .../SupporterRunnerPostQueryRepository.java | 39 + .../query/service/RunnerQueryService.java | 21 + .../query/service/SupporterQueryService.java | 21 + .../notification/command/Notification.java | 146 +++ .../NotificationCommandController.java | 38 + .../event/NotificationEventListener.java | 82 ++ .../NotificationCommandRepository.java | 7 + .../service/NotificationCommandService.java | 35 + .../notification/command/vo/IsRead.java | 35 + .../command/vo/NotificationMessage.java | 32 + .../command/vo/NotificationReferencedId.java | 30 + .../command/vo/NotificationTitle.java | 32 + .../command/vo/NotificationType.java | 6 + .../NotificationBusinessException.java | 10 + .../NotificationDomainException.java | 10 + .../NotificationRequestException.java | 11 + .../NotificationQueryController.java | 31 + .../response/NotificationResponse.java | 31 + .../response/NotificationResponses.java | 19 + .../NotificationQuerydslRepository.java | 27 + .../service/NotificationQueryService.java | 21 + .../oauth/command/AuthorizationHeader.java | 27 + .../oauth/command/OauthInformation.java | 48 + .../baton/domain/oauth/command/OauthType.java | 12 + .../authcode/AuthCodeRequestUrlProvider.java | 10 + .../AuthCodeRequestUrlProviderComposite.java | 37 + .../client/OauthInformationClient.java | 11 + .../OauthInformationClientComposite.java | 36 + .../controller/OauthCommandController.java | 106 +++ .../exception/OauthBusinessException.java | 10 + .../exception/OauthRequestException.java | 11 + .../OauthMemberCommandRepository.java | 15 + .../OauthRunnerCommandRepository.java | 20 + .../OauthSupporterCommandRepository.java | 20 + .../RefreshTokenCommandRepository.java | 17 + .../command/service/OauthCommandService.java | 166 ++++ .../oauth/command/token/AccessToken.java | 15 + .../oauth/command/token/ExpireDate.java | 40 + .../oauth/command/token/RefreshToken.java | 77 ++ .../oauth/command/token/SocialToken.java | 15 + .../domain/oauth/command/token/Token.java | 31 + .../domain/oauth/command/token/Tokens.java | 7 + .../RefreshTokenDomainException.java | 10 + .../resolver/AuthMemberPrincipal.java | 13 + .../AuthMemberPrincipalArgumentResolver.java | 54 ++ .../resolver/AuthRunnerPrincipal.java | 13 + .../AuthRunnerPrincipalArgumentResolver.java | 57 ++ .../resolver/AuthSupporterPrincipal.java | 13 + ...uthSupporterPrincipalArgumentResolver.java | 50 + .../UserPrincipalArgumentResolver.java | 61 ++ .../domain/runnerpost/command/RunnerPost.java | 299 ++++++ .../command/RunnerPostsApplicantCount.java | 46 + .../RunnerPostCommandController.java | 106 +++ .../event/RunnerPostApplySupporterEvent.java | 4 + .../event/RunnerPostAssignSupporterEvent.java | 4 + .../RunnerPostReviewStatusDoneEvent.java | 4 + .../RunnerPostBusinessException.java | 10 + .../exception/RunnerPostDomainException.java | 10 + .../exception/RunnerPostRequestException.java | 11 + .../exception/validator/FutureValidator.java | 27 + .../validator/MaxLengthValidator.java | 28 + .../exception/validator/UrlValidator.java | 34 + .../exception/validator/ValidFuture.java | 28 + .../exception/validator/ValidMaxLength.java | 30 + .../exception/validator/ValidNotUrl.java | 28 + .../RunnerPostCommandRepository.java | 7 + .../dto/RunnerPostApplicantCountDto.java | 4 + .../service/RunnerPostCommandService.java | 177 ++++ .../dto/RunnerPostApplicantCreateRequest.java | 9 + .../service/dto/RunnerPostCreateRequest.java | 33 + .../service/dto/RunnerPostUpdateRequest.java | 12 + .../command/vo/CuriousContents.java | 32 + .../runnerpost/command/vo/Deadline.java | 38 + .../command/vo/ImplementedContents.java | 32 + .../runnerpost/command/vo/IsReviewed.java | 35 + .../command/vo/PostscriptContents.java | 32 + .../runnerpost/command/vo/PullRequestUrl.java | 34 + .../runnerpost/command/vo/ReviewStatus.java | 31 + .../domain/runnerpost/command/vo/Title.java | 32 + .../runnerpost/command/vo/WatchedCount.java | 35 + .../controller/RunnerPostQueryController.java | 128 +++ .../response/RunnerPostResponse.java | 150 +++ .../response/SupporterRunnerPostResponse.java | 37 + .../SupporterRunnerPostResponses.java | 13 + .../repository/RunnerPostPageRepository.java | 123 +++ .../repository/RunnerPostQueryRepository.java | 57 ++ .../dto/ApplicantCountMappingDto.java | 10 + .../query/service/RunnerPostQueryService.java | 136 +++ .../domain/tag/command/RunnerPostTag.java | 61 ++ .../domain/tag/command/RunnerPostTags.java | 49 + .../touch/baton/domain/tag/command/Tag.java | 61 ++ .../repository/TagCommandRepository.java | 12 + .../domain/tag/command/vo/TagReducedName.java | 53 ++ .../query/controller/TagQueryController.java | 31 + .../response/TagSearchResponse.java | 12 + .../response/TagSearchResponses.java | 19 + .../RunnerPostTagQueryRepository.java | 19 + .../repository/TagQuerydslRepository.java | 26 + .../tag/query/service/TagQueryService.java | 27 + .../command/RunnerTechnicalTag.java | 63 ++ .../command/RunnerTechnicalTags.java | 29 + .../command/SupporterTechnicalTag.java | 63 ++ .../command/SupporterTechnicalTags.java | 29 + .../technicaltag/command/TechnicalTag.java | 46 + .../RunnerTechnicalTagCommandRepository.java | 15 + ...upporterTechnicalTagCommandRepository.java | 15 + .../TechnicalTagQueryRepository.java | 12 + .../baton/infra/auth/jwt/JwtDecoder.java | 4 +- .../GithubAuthCodeRequestUrlProvider.java | 4 +- .../client/GithubInformationClient.java | 6 +- .../github/response/GithubMemberResponse.java | 17 +- .../infra/github/GithubBranchManager.java | 2 +- .../baton/src/main/resources/application.yml | 1 + ...V20231007_1__create_table_notification.sql | 13 + ...alter_table_notification_constraint_fk.sql | 3 + ...ate_review_status_index_on_runner_post.sql | 2 + .../baton/assure/common/AssuredSupport.java | 19 + .../baton/assure/common/JwtTestManager.java | 2 +- .../assure/common/OauthLoginTestManager.java | 3 +- .../SupporterFeedbackCreateAssuredTest.java | 103 +++ ...SupporterFeedbackCreateAssuredSupport.java | 68 ++ .../command/MemberBranchAssuredTest.java | 28 + .../command/RunnerUpdateAssuredTest.java | 131 +++ .../command/SupporterUpdateAssuredTest.java | 101 ++ .../member/query/MemberQueryAssuredTest.java | 30 + .../member/query/RunnerQueryAssuredTest.java | 33 + .../query/SupporterQueryAssuredTest.java | 52 ++ .../MemberBranchCreateAssuredSupport.java | 59 ++ .../command/RunnerUpdateAssuredSupport.java | 96 ++ .../SupporterUpdateAssuredSupport.java | 78 ++ .../query/MemberQueryAssuredSupport.java | 63 ++ .../query/RunnerQueryAssuredSupport.java | 103 +++ .../query/SupporterQueryAssuredSupport.java | 111 +++ .../NotificationDeleteAssuredTest.java | 73 ++ .../NotificationUpdateAssuredTest.java | 73 ++ .../query/NotificationQueryAssuredTest.java | 91 ++ .../command/NotificationDeleteSupport.java | 59 ++ .../command/NotificationUpdateSupport.java | 59 ++ .../query/NotificationQuerySupport.java | 60 ++ .../assure/oauth/OauthAssuredSupport.java | 32 +- .../assure/oauth/OauthCreateAssuredTest.java | 6 +- .../assure/oauth/OauthDeleteAssuredTest.java | 67 ++ .../oauth/OauthRefreshTokenAssuredTest.java | 26 +- .../repository/TestMemberQueryRepository.java | 33 + .../TestNotificationCommandRepository.java | 6 + .../TestRefreshTokenRepository.java | 6 +- .../TestRunnerPostQueryRepository.java | 27 + .../repository/TestRunnerQueryRepository.java | 25 + .../TestSupporterQueryRepository.java | 25 + ...estSupporterRunnerPostQueryRepository.java | 12 + .../repository/TestTagQuerydslRepository.java | 13 + .../TestTechnicalTagQueryRepository.java | 6 + .../command/RunnerPostCreateAssuredTest.java | 258 ++++++ .../command/RunnerPostDeleteAssuredTest.java | 174 ++++ .../command/RunnerPostUpdateAssuredTest.java | 113 +++ .../RunnerPostApplicantCreateAssuredTest.java | 97 ++ .../RunnerPostApplicantDeleteAssuredTest.java | 69 ++ .../RunnerPostApplicantAssuredTest.java | 84 ++ .../RunnerPostCountByRunnerAssuredTest.java | 60 ++ ...RunnerPostCountBySupporterAssuredTest.java | 188 ++++ ...nnerPostInRunnerPostDetailAssuredTest.java | 97 ++ .../query/page/RunnerPostPageAssuredTest.java | 262 ++++++ .../RunnerPostPageRunnerAssuredTest.java | 116 +++ .../RunnerPostPageSupporterAssuredTest.java | 253 ++++++ .../support/RunnerPostPageSupport.java | 179 ++++ .../command/RunnerPostCreateSupport.java | 99 ++ .../command/RunnerPostDeleteSupport.java | 60 ++ .../command/RunnerPostUpdateSupport.java | 81 ++ .../RunnerPostApplicantCreateSupport.java | 78 ++ ...nnerPostApplicantDeleteAssuredSupport.java | 60 ++ .../RunnerPostApplicantQuerySupport.java | 117 +++ .../RunnerPostCountByRunnerSupport.java | 68 ++ .../RunnerPostCountBySupporterSupport.java | 78 ++ .../query/detail/RunnerPostDetailSupport.java | 117 +++ .../runner/RunnerPostPageRunnerSupport.java | 118 +++ .../RunnerPostPageSupporterSupport.java | 146 +++ .../assure/tag/query/TagReadAssuredTest.java | 59 ++ .../tag/support/query/TagQuerySupport.java | 58 ++ .../RunnerPostDeadlineCheckSchedulerTest.java | 10 +- ...ScheduleRunnerPostQueryRepositoryTest.java | 108 +++ .../touch/baton/config/AssuredTestConfig.java | 33 +- .../config/QueryDslRepositoryTestConfig.java | 37 + .../baton/config/RepositoryTestConfig.java | 82 +- .../touch/baton/config/RestdocsConfig.java | 127 ++- .../touch/baton/config/ServiceTestConfig.java | 76 +- .../config/converter/ConverterConfigTest.java | 6 +- .../infra/auth/MockBeanAuthTestConfig.java | 16 + .../config/infra/auth/jwt/FakeJwtConfig.java | 34 + .../auth/oauth/MockRefreshTokenConfig.java | 2 +- .../auth/oauth/authcode/FakeAuthCodes.java | 20 + ...CodeRequestUrlProviderCompositeConfig.java | 4 +- ...kAuthInformationClientCompositeConfig.java | 52 ++ .../document/github/GithubBranchApiTest.java | 32 +- .../delete/NotificationDeleteApiTest.java | 57 ++ ...ificationReadWithLoginedMemberApiTest.java | 83 ++ .../update/NotificationUpdateApiTest.java | 57 ++ .../document/oauth/OauthLogoutApiTest.java | 39 + .../oauth/github/GithubOauthApiTest.java | 57 +- .../oauth/token/RefreshTokenApiTest.java | 35 +- .../MemberReadWithLoginedMemberApiTest.java | 18 +- .../runner/read/RunnerReadByGuestApiTest.java | 33 +- .../RunnerReadSimpleByRunnerIdApiTest.java | 65 ++ .../RunnerReadWithLoginedRunnerApiTest.java | 27 +- .../runner/update/RunnerUpdateApiTest.java | 33 +- .../read/SupporterReadByGuestApiTest.java | 30 +- .../update/SupporterUpdateApiTest.java | 28 +- .../create/RunnerPostApplicantApiTest.java | 43 +- .../create/RunnerPostCreateApiTest.java | 32 +- .../delete/RunnerPostDeleteApiTest.java | 23 +- ...nerPostCountOfSupporterByGuestApiTest.java | 72 ++ ...nnerPostCountWithLoginedRunnerApiTest.java | 80 ++ ...rPostCountWithLoginedSupporterApiTest.java | 84 ++ ...nnerPostReadOfSupporterByGuestApiTest.java | 108 +-- .../read/RunnerPostReadOneApiTest.java | 36 +- .../read/RunnerPostReadSearchApiTest.java | 82 +- ...unnerPostReadWithLoginedRunnerApiTest.java | 115 +++ ...erPostReadWithLoginedSupporterApiTest.java | 113 +-- ...PostUpdateApplicantCancelationApiTest.java | 31 +- .../read/SupporterRunnerPostReadApiTest.java | 37 +- .../runnerpost/read/TagReadApiTest.java | 21 +- .../update/RunnerPostUpdateApiTest.java | 37 +- .../domain/common/vo/DescriptionTest.java | 2 +- .../baton/domain/common/vo/TitleTest.java | 1 + .../domain/common/vo/WatchedCountTest.java | 1 + ...upporterFeedbackCommandRepositoryTest.java | 66 ++ .../service/FeedbackCommandServiceTest.java | 94 ++ .../touch/baton/domain/member/MemberTest.java | 33 +- .../baton/domain/member/vo/CompanyTest.java | 1 + .../baton/domain/member/vo/GithubUrlTest.java | 1 + .../baton/domain/member/vo/ImageUrlTest.java | 1 + .../domain/member/vo/MemberNameTest.java | 20 + .../baton/domain/member/vo/OauthIdTest.java | 1 + .../baton/domain/member/vo/SocialIdTest.java | 1 + .../command/NotificationTest.java | 207 +++++ .../event/NotificationEventListenerTest.java | 148 +++ .../NotificationCommandRepositoryTest.java | 39 + .../NotificationCommandServiceTest.java | 99 ++ .../notification/command/vo/IsReadTest.java | 44 + .../command/vo/NotificationMessageTest.java | 16 + .../vo/NotificationReferencedIdTest.java | 16 + .../command/vo/NotificationTitleTest.java | 16 + .../NotificationQuerydslRepositoryTest.java | 71 ++ .../service/NotificationQueryServiceTest.java | 77 ++ .../controller/OauthTypeConverterTest.java | 26 + .../RefreshTokenCommandRepositoryTest.java | 110 +++ .../OauthCommandServiceDeleteTest.java | 72 ++ .../OauthCommandServiceUpdateTest.java | 193 ++++ .../oauth/command/token/RefreshTokenTest.java | 157 ++++ .../oauth/vo/AuthorizationHeaderTest.java | 6 +- .../touch/baton/domain/runner/RunnerTest.java | 19 +- .../repository/RunnerQueryRepositoryTest.java | 92 ++ .../service/RunnerCommandServiceTest.java | 69 ++ .../service/RunnerQueryServiceTest.java | 58 ++ .../domain/runnerpost/RunnerPostTest.java | 65 +- .../RunnerPostsApplicantCountTest.java | 74 ++ .../exception/validator/UrlValidatorTest.java | 121 +++ .../RunnerPostRepositoryDeleteTest.java | 92 ++ .../RunnerPostCommandServiceCreateTest.java | 167 ++++ .../RunnerPostCommandServiceDeleteTest.java | 119 +++ .../RunnerPostCommandServiceEventTest.java | 119 +++ .../RunnerPostCommandServiceUpdateTest.java | 207 +++++ ...UpdateApplicantCancelationServiceTest.java | 111 +++ .../command/vo/CuriousContentsTest.java | 16 + .../runnerpost/command/vo/DeadlineTest.java | 16 + .../command/vo/ImplementedContentsTest.java | 16 + .../runnerpost/command/vo/IsReviewedTest.java | 21 + .../command/vo/PostscriptContentsTest.java | 16 + .../command/vo/PullRequestUrlTest.java | 16 + .../RunnerPostPageRepositoryTest.java | 501 ++++++++++ .../RunnerPostQueryRepositoryReadTest.java | 117 +++ .../RunnerPostQueryRepositoryTest.java | 197 ++++ .../service/RunnerPostQueryServiceTest.java | 859 ++++++++++++++++++ .../command/SupporterFeedbackTest.java | 157 ++++ .../supporter/command/SupporterTest.java | 156 ++++ .../SupporterQueryRepositoryTest.java | 41 + ...porterRunnerPostCommandRepositoryTest.java | 79 ++ .../service/SupporterCommandServiceTest.java | 40 + ...upporterRunnerPostQueryRepositoryTest.java | 112 +++ .../service/SupporterQueryServiceTest.java | 45 + .../domain/tag/command/RunnerPostTagTest.java | 120 +++ .../tag/command/RunnerPostTagsTest.java | 54 ++ .../baton/domain/tag/command/TagTest.java | 50 + .../tag/command/vo/TagReducedNameTest.java | 45 + .../RunnerPostTagQueryRepositoryTest.java | 57 ++ .../repository/TagQuerydslRepositoryTest.java | 103 +++ .../query/service/TagQueryServiceTest.java | 77 ++ .../SupporterTechnicalTagTest.java | 8 +- .../SupporterTechnicalTagsTest.java | 10 +- .../domain/technicaltag/TechnicalTagTest.java | 1 + ...porterTechnicalTagQueryRepositoryTest.java | 59 ++ .../TechnicalTagQueryRepositoryTest.java | 53 ++ .../baton/fixture/domain/MemberFixture.java | 14 +- .../fixture/domain/NotificationFixture.java | 27 + .../fixture/domain/RefreshTokenFixture.java | 8 +- .../baton/fixture/domain/RunnerFixture.java | 12 +- .../fixture/domain/RunnerPostFixture.java | 69 +- .../fixture/domain/RunnerPostTagFixture.java | 6 +- .../fixture/domain/RunnerPostTagsFixture.java | 4 +- .../domain/RunnerTechnicalTagsFixture.java | 8 +- .../domain/SupporterFeedbackFixture.java | 12 +- .../fixture/domain/SupporterFixture.java | 12 +- .../domain/SupporterRunnerPostFixture.java | 8 +- .../domain/SupporterTechnicalTagFixture.java | 6 +- .../domain/SupporterTechnicalTagsFixture.java | 4 +- .../baton/fixture/domain/TagFixture.java | 4 +- .../fixture/domain/TechnicalTagFixture.java | 2 +- .../vo/AuthorizationHeaderFixture.java | 2 +- .../baton/fixture/vo/CompanyFixture.java | 2 +- .../fixture/vo/CuriousContentsFixture.java | 2 +- .../baton/fixture/vo/DeadlineFixture.java | 2 +- .../baton/fixture/vo/DescriptionFixture.java | 2 +- .../baton/fixture/vo/ExpireDateFixture.java | 2 +- .../baton/fixture/vo/GithubUrlFixture.java | 2 +- .../baton/fixture/vo/ImageUrlFixture.java | 2 +- .../vo/ImplementedContentsFixture.java | 2 +- .../baton/fixture/vo/IntroductionFixture.java | 2 +- .../baton/fixture/vo/MemberNameFixture.java | 2 +- .../baton/fixture/vo/MessageFixture.java | 2 +- .../vo/NotificationMessageFixture.java | 13 + .../vo/NotificationReferencedIdFixture.java | 13 + .../fixture/vo/NotificationTitleFixture.java | 13 + .../baton/fixture/vo/OauthIdFixture.java | 2 +- .../fixture/vo/PostscriptContentsFixture.java | 2 +- .../fixture/vo/PullRequestUrlFixture.java | 2 +- .../baton/fixture/vo/ReviewCountFixture.java | 2 +- .../baton/fixture/vo/SocialIdFixture.java | 2 +- .../touch/baton/fixture/vo/TitleFixture.java | 2 +- .../touch/baton/fixture/vo/TokenFixture.java | 2 +- .../baton/fixture/vo/WatchedCountFixture.java | 2 +- .../auth/jwt/JwtEncoderAndDecoderTest.java | 2 +- 401 files changed, 18477 insertions(+), 985 deletions(-) create mode 100644 backend/baton/src/docs/asciidoc/NotificationDeleteApi.adoc create mode 100644 backend/baton/src/docs/asciidoc/NotificationLoginReadApi.adoc create mode 100644 backend/baton/src/docs/asciidoc/NotificationUpdateApi.adoc create mode 100644 backend/baton/src/docs/asciidoc/OauthLogoutApi.adoc create mode 100644 backend/baton/src/main/java/touch/baton/config/QuerydslConfig.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/common/TruncatedBaseEntity.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/common/request/PageParams.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/common/response/IdExtractable.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/feedback/command/SupporterFeedback.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/feedback/command/controller/FeedbackCommandController.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/feedback/command/repository/SupporterFeedbackCommandRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/feedback/command/service/FeedbackCommandService.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/feedback/command/service/dto/SupporterFeedBackCreateRequest.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/feedback/command/vo/Description.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/feedback/command/vo/ReviewType.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/Member.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/Runner.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/Supporter.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/SupporterRunnerPost.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/controller/MemberBranchController.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/controller/RunnerCommandController.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/controller/SupporterCommandController.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/controller/response/LoginMemberInfoResponse.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/repository/MemberCommandRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/repository/SupporterCommandRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/repository/SupporterRunnerPostCommandRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/service/GithubBranchManageable.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/service/RunnerCommandService.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/service/SupporterCommandService.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/service/dto/GithubRepoNameRequest.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/service/dto/RunnerUpdateRequest.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/service/dto/SupporterUpdateRequest.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/vo/Company.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/vo/GithubUrl.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/vo/ImageUrl.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/vo/Introduction.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/vo/MemberName.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/vo/Message.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/vo/OauthId.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/vo/ReviewCount.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/command/vo/SocialId.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/exception/RunnerBusinessException.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/exception/RunnerDomainException.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/exception/RunnerRequestException.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/exception/SupporterBusinessException.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/exception/SupporterDomainException.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/exception/SupporterRequestException.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/query/controller/MemberQueryController.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/query/controller/RunnerQueryController.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/query/controller/SupporterQueryController.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/query/controller/response/RunnerResponse.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/query/controller/response/SupporterResponse.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/query/repository/RunnerQueryRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/query/repository/SupporterQueryRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/query/repository/SupporterRunnerPostQueryRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/query/service/RunnerQueryService.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/member/query/service/SupporterQueryService.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/notification/command/Notification.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/notification/command/controller/NotificationCommandController.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/notification/command/event/NotificationEventListener.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/notification/command/repository/NotificationCommandRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/notification/command/service/NotificationCommandService.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/notification/command/vo/IsRead.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/notification/command/vo/NotificationMessage.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/notification/command/vo/NotificationReferencedId.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/notification/command/vo/NotificationTitle.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/notification/command/vo/NotificationType.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/notification/exception/NotificationBusinessException.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/notification/exception/NotificationDomainException.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/notification/exception/NotificationRequestException.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/notification/query/controller/NotificationQueryController.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/notification/query/controller/response/NotificationResponse.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/notification/query/controller/response/NotificationResponses.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/notification/query/repository/NotificationQuerydslRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/notification/query/service/NotificationQueryService.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/AuthorizationHeader.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/OauthInformation.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/OauthType.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/authcode/AuthCodeRequestUrlProvider.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/authcode/AuthCodeRequestUrlProviderComposite.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/client/OauthInformationClient.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/client/OauthInformationClientComposite.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/controller/OauthCommandController.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/exception/OauthBusinessException.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/exception/OauthRequestException.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/repository/OauthMemberCommandRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/repository/OauthRunnerCommandRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/repository/OauthSupporterCommandRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/repository/RefreshTokenCommandRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/service/OauthCommandService.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/token/AccessToken.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/token/ExpireDate.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/token/RefreshToken.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/token/SocialToken.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/token/Token.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/token/Tokens.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/command/token/exception/RefreshTokenDomainException.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthMemberPrincipal.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthMemberPrincipalArgumentResolver.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthRunnerPrincipal.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthRunnerPrincipalArgumentResolver.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthSupporterPrincipal.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthSupporterPrincipalArgumentResolver.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/UserPrincipalArgumentResolver.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/RunnerPost.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/RunnerPostsApplicantCount.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/controller/RunnerPostCommandController.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/event/RunnerPostApplySupporterEvent.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/event/RunnerPostAssignSupporterEvent.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/event/RunnerPostReviewStatusDoneEvent.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/RunnerPostBusinessException.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/RunnerPostDomainException.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/RunnerPostRequestException.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/FutureValidator.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/MaxLengthValidator.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/UrlValidator.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/ValidFuture.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/ValidMaxLength.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/ValidNotUrl.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/repository/RunnerPostCommandRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/repository/dto/RunnerPostApplicantCountDto.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/service/RunnerPostCommandService.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/service/dto/RunnerPostApplicantCreateRequest.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/service/dto/RunnerPostCreateRequest.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/service/dto/RunnerPostUpdateRequest.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/CuriousContents.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/Deadline.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/ImplementedContents.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/IsReviewed.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/PostscriptContents.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/PullRequestUrl.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/ReviewStatus.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/Title.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/WatchedCount.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/query/controller/RunnerPostQueryController.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/query/controller/response/RunnerPostResponse.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/query/controller/response/SupporterRunnerPostResponse.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/query/controller/response/SupporterRunnerPostResponses.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/query/repository/RunnerPostPageRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/query/repository/RunnerPostQueryRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/query/repository/dto/ApplicantCountMappingDto.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/runnerpost/query/service/RunnerPostQueryService.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/tag/command/RunnerPostTag.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/tag/command/RunnerPostTags.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/tag/command/Tag.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/tag/command/repository/TagCommandRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/tag/command/vo/TagReducedName.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/tag/query/controller/TagQueryController.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/tag/query/controller/response/TagSearchResponse.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/tag/query/controller/response/TagSearchResponses.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/tag/query/repository/RunnerPostTagQueryRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/tag/query/repository/TagQuerydslRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/tag/query/service/TagQueryService.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/technicaltag/command/RunnerTechnicalTag.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/technicaltag/command/RunnerTechnicalTags.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/technicaltag/command/SupporterTechnicalTag.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/technicaltag/command/SupporterTechnicalTags.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/technicaltag/command/TechnicalTag.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/technicaltag/command/repository/RunnerTechnicalTagCommandRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/technicaltag/command/repository/SupporterTechnicalTagCommandRepository.java create mode 100644 backend/baton/src/main/java/touch/baton/domain/technicaltag/query/repository/TechnicalTagQueryRepository.java create mode 100644 backend/baton/src/main/resources/db/migration/V20231007_1__create_table_notification.sql create mode 100644 backend/baton/src/main/resources/db/migration/V20231007_2__alter_table_notification_constraint_fk.sql create mode 100644 backend/baton/src/main/resources/db/migration/V20231011__create_review_status_index_on_runner_post.sql create mode 100644 backend/baton/src/test/java/touch/baton/assure/feedback/command/SupporterFeedbackCreateAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/feedback/support/command/SupporterFeedbackCreateAssuredSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/member/command/MemberBranchAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/member/command/RunnerUpdateAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/member/command/SupporterUpdateAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/member/query/MemberQueryAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/member/query/RunnerQueryAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/member/query/SupporterQueryAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/member/support/command/MemberBranchCreateAssuredSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/member/support/command/RunnerUpdateAssuredSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/member/support/command/SupporterUpdateAssuredSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/member/support/query/MemberQueryAssuredSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/member/support/query/RunnerQueryAssuredSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/member/support/query/SupporterQueryAssuredSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/notification/command/NotificationDeleteAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/notification/command/NotificationUpdateAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/notification/query/NotificationQueryAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/notification/support/command/NotificationDeleteSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/notification/support/command/NotificationUpdateSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/notification/support/query/NotificationQuerySupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/oauth/OauthDeleteAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/repository/TestMemberQueryRepository.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/repository/TestNotificationCommandRepository.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/repository/TestRunnerPostQueryRepository.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/repository/TestRunnerQueryRepository.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/repository/TestSupporterQueryRepository.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/repository/TestSupporterRunnerPostQueryRepository.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/repository/TestTagQuerydslRepository.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/repository/TestTechnicalTagQueryRepository.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/command/RunnerPostCreateAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/command/RunnerPostDeleteAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/command/RunnerPostUpdateAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/command/applicant/RunnerPostApplicantCreateAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/command/applicant/RunnerPostApplicantDeleteAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/query/applicant/RunnerPostApplicantAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/query/count/runner/RunnerPostCountByRunnerAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/query/count/supporter/RunnerPostCountBySupporterAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/query/detail/RunnerPostInRunnerPostDetailAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/query/page/RunnerPostPageAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/query/page/runner/RunnerPostPageRunnerAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/query/page/supporter/RunnerPostPageSupporterAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/support/RunnerPostPageSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/support/command/RunnerPostCreateSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/support/command/RunnerPostDeleteSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/support/command/RunnerPostUpdateSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/support/command/applicant/RunnerPostApplicantCreateSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/support/command/applicant/RunnerPostApplicantDeleteAssuredSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/applicant/RunnerPostApplicantQuerySupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/count/runner/RunnerPostCountByRunnerSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/count/supporter/RunnerPostCountBySupporterSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/detail/RunnerPostDetailSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/page/runner/RunnerPostPageRunnerSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/page/supporter/RunnerPostPageSupporterSupport.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/tag/query/TagReadAssuredTest.java create mode 100644 backend/baton/src/test/java/touch/baton/assure/tag/support/query/TagQuerySupport.java create mode 100644 backend/baton/src/test/java/touch/baton/common/schedule/ScheduleRunnerPostQueryRepositoryTest.java create mode 100644 backend/baton/src/test/java/touch/baton/config/QueryDslRepositoryTestConfig.java create mode 100644 backend/baton/src/test/java/touch/baton/config/infra/auth/MockBeanAuthTestConfig.java create mode 100644 backend/baton/src/test/java/touch/baton/config/infra/auth/jwt/FakeJwtConfig.java create mode 100644 backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/authcode/FakeAuthCodes.java create mode 100644 backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/client/MockAuthInformationClientCompositeConfig.java create mode 100644 backend/baton/src/test/java/touch/baton/document/notification/delete/NotificationDeleteApiTest.java create mode 100644 backend/baton/src/test/java/touch/baton/document/notification/read/NotificationReadWithLoginedMemberApiTest.java create mode 100644 backend/baton/src/test/java/touch/baton/document/notification/update/NotificationUpdateApiTest.java create mode 100644 backend/baton/src/test/java/touch/baton/document/oauth/OauthLogoutApiTest.java create mode 100644 backend/baton/src/test/java/touch/baton/document/profile/runner/read/RunnerReadSimpleByRunnerIdApiTest.java create mode 100644 backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostCountOfSupporterByGuestApiTest.java create mode 100644 backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostCountWithLoginedRunnerApiTest.java create mode 100644 backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostCountWithLoginedSupporterApiTest.java create mode 100644 backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadWithLoginedRunnerApiTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/feedback/repository/SupporterFeedbackCommandRepositoryTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/feedback/service/FeedbackCommandServiceTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/member/vo/MemberNameTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/notification/command/NotificationTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/notification/command/event/NotificationEventListenerTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/notification/command/repository/NotificationCommandRepositoryTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/notification/command/service/NotificationCommandServiceTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/notification/command/vo/IsReadTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/notification/command/vo/NotificationMessageTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/notification/command/vo/NotificationReferencedIdTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/notification/command/vo/NotificationTitleTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/notification/query/repository/NotificationQuerydslRepositoryTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/notification/query/service/NotificationQueryServiceTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/oauth/command/controller/OauthTypeConverterTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/oauth/command/repository/RefreshTokenCommandRepositoryTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/oauth/command/service/OauthCommandServiceDeleteTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/oauth/command/service/OauthCommandServiceUpdateTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/oauth/command/token/RefreshTokenTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runner/repository/RunnerQueryRepositoryTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runner/service/RunnerCommandServiceTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runner/service/RunnerQueryServiceTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/RunnerPostsApplicantCountTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/command/exception/validator/UrlValidatorTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/command/repository/RunnerPostRepositoryDeleteTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/command/service/RunnerPostCommandServiceCreateTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/command/service/RunnerPostCommandServiceDeleteTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/command/service/RunnerPostCommandServiceEventTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/command/service/RunnerPostCommandServiceUpdateTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/command/service/RunnerPostUpdateApplicantCancelationServiceTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/command/vo/CuriousContentsTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/command/vo/DeadlineTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/command/vo/ImplementedContentsTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/command/vo/IsReviewedTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/command/vo/PostscriptContentsTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/command/vo/PullRequestUrlTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/query/repository/RunnerPostPageRepositoryTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/query/repository/RunnerPostQueryRepositoryReadTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/query/repository/RunnerPostQueryRepositoryTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/runnerpost/query/service/RunnerPostQueryServiceTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/supporter/command/SupporterFeedbackTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/supporter/command/SupporterTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/supporter/command/repository/SupporterQueryRepositoryTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/supporter/command/repository/SupporterRunnerPostCommandRepositoryTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/supporter/command/service/SupporterCommandServiceTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/supporter/query/repository/SupporterRunnerPostQueryRepositoryTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/supporter/query/service/SupporterQueryServiceTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/tag/command/RunnerPostTagTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/tag/command/RunnerPostTagsTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/tag/command/TagTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/tag/command/vo/TagReducedNameTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/tag/query/repository/RunnerPostTagQueryRepositoryTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/tag/query/repository/TagQuerydslRepositoryTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/tag/query/service/TagQueryServiceTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/technicaltag/repository/SupporterTechnicalTagQueryRepositoryTest.java create mode 100644 backend/baton/src/test/java/touch/baton/domain/technicaltag/repository/TechnicalTagQueryRepositoryTest.java create mode 100644 backend/baton/src/test/java/touch/baton/fixture/domain/NotificationFixture.java create mode 100644 backend/baton/src/test/java/touch/baton/fixture/vo/NotificationMessageFixture.java create mode 100644 backend/baton/src/test/java/touch/baton/fixture/vo/NotificationReferencedIdFixture.java create mode 100644 backend/baton/src/test/java/touch/baton/fixture/vo/NotificationTitleFixture.java diff --git a/.github/workflows/deploy-be-ci-cd-push.yml b/.github/workflows/deploy-be-ci-cd-push.yml index c30277920..01e58b0a7 100644 --- a/.github/workflows/deploy-be-ci-cd-push.yml +++ b/.github/workflows/deploy-be-ci-cd-push.yml @@ -53,13 +53,9 @@ jobs: - name: Pull Latest Docker Image run: | sudo docker login --username ${{ secrets.DOCKERHUB_DEPLOY_USERNAME }} --password ${{ secrets.DOCKERHUB_DEPLOY_TOKEN }} - if sudo docker inspect spring-baton &>/dev/null; then - sudo docker stop spring-baton - sudo docker rm -f spring-baton - sudo docker image prune -af - fi sudo docker pull 2023batondeploy/2023-baton-deploy:latest - name: Docker Compose run: | - sudo docker run --name spring-baton -v /home/ubuntu/logs:/app/logs -p 8080:8080 -e TZ=Asia/Seoul 2023batondeploy/2023-baton-deploy:latest 1>> build.log 2>> error.log & + /home/ubuntu/zero-downtime-deploy.sh + sudo docker image prune -af diff --git a/.github/workflows/dev-be-ci-cd-push.yml b/.github/workflows/dev-be-ci-cd-push.yml index b4d331781..62a461ff1 100644 --- a/.github/workflows/dev-be-ci-cd-push.yml +++ b/.github/workflows/dev-be-ci-cd-push.yml @@ -50,16 +50,13 @@ jobs: needs: build steps: + - name: Pull Latest Docker Image run: | sudo docker login --username ${{ secrets.DOCKERHUB_DEV_USERNAME }} --password ${{ secrets.DOCKERHUB_DEV_TOKEN }} - if sudo docker inspect spring-baton &>/dev/null; then - sudo docker stop spring-baton - sudo docker rm -f spring-baton - sudo docker image prune -af - fi sudo docker pull 2023baton/2023baton:latest - name: Docker Compose run: | - sudo docker run --name spring-baton --network=baton -p 8080:8080 -e TZ=Asia/Seoul 2023baton/2023baton:latest 1>> build.log 2>> error.log & + /home/ubuntu/zero-downtime-deploy.sh + sudo docker image prune -af diff --git a/backend/baton/.gitignore b/backend/baton/.gitignore index 121ef1a81..7f4296e4d 100644 --- a/backend/baton/.gitignore +++ b/backend/baton/.gitignore @@ -180,4 +180,5 @@ gradle-app.setting src/main/resources/application-deploy.yml src/main/resources/application-dev.yml +src/main/resources/application-local.yml src/main/resources/static/docs/** diff --git a/backend/baton/build.gradle b/backend/baton/build.gradle index 58d99a37d..559beafe2 100644 --- a/backend/baton/build.gradle +++ b/backend/baton/build.gradle @@ -46,6 +46,12 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springframework.boot:spring-boot-starter-aop:3.1.1' + // Querydsl 추가 + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + // flyway implementation 'org.flywaydb:flyway-core' implementation 'org.flywaydb:flyway-mysql' diff --git a/backend/baton/src/docs/asciidoc/GithubOauthApi.adoc b/backend/baton/src/docs/asciidoc/GithubOauthApi.adoc index ab6c85a3c..0f5ca4360 100644 --- a/backend/baton/src/docs/asciidoc/GithubOauthApi.adoc +++ b/backend/baton/src/docs/asciidoc/GithubOauthApi.adoc @@ -14,24 +14,16 @@ endif::[] ===== *Http Request* -include::{snippets}/../../build/generated-snippets/github-oauth-api-test/github_login/http-request.adoc[] +include::{snippets}/../../build/generated-snippets/github-oauth-api-test/github_redirect_auth_code/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[] +include::{snippets}/../../build/generated-snippets/github-oauth-api-test/github_redirect_auth_code/path-parameters.adoc[] ===== *Http Response* -include::{snippets}/../../build/generated-snippets/github-oauth-api-test/github_login/http-response.adoc[] +include::{snippets}/../../build/generated-snippets/github-oauth-api-test/github_redirect_auth_code/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[] +include::{snippets}/../../build/generated-snippets/github-oauth-api-test/github_redirect_auth_code/response-headers.adoc[] diff --git a/backend/baton/src/docs/asciidoc/NotificationDeleteApi.adoc b/backend/baton/src/docs/asciidoc/NotificationDeleteApi.adoc new file mode 100644 index 000000000..6510a9940 --- /dev/null +++ b/backend/baton/src/docs/asciidoc/NotificationDeleteApi.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/notification-delete-api-test/delete-notification-by-notification-id/http-request.adoc[] + +===== *Http Request Headers* + +include::{snippets}/../../build/generated-snippets/notification-delete-api-test/delete-notification-by-notification-id/request-headers.adoc[] + +===== *Http Request Path Parameters* + +include::{snippets}/../../build/generated-snippets/notification-delete-api-test/delete-notification-by-notification-id/path-parameters.adoc[] + +===== *Http Response* + +include::{snippets}/../../build/generated-snippets/notification-delete-api-test/delete-notification-by-notification-id/http-response.adoc[] diff --git a/backend/baton/src/docs/asciidoc/NotificationLoginReadApi.adoc b/backend/baton/src/docs/asciidoc/NotificationLoginReadApi.adoc new file mode 100644 index 000000000..6102b83fa --- /dev/null +++ b/backend/baton/src/docs/asciidoc/NotificationLoginReadApi.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/notification-read-with-logined-member-api-test/read-notifications-by-member-id/http-request.adoc[] + +===== *Http Request Headers* + +include::{snippets}/../../build/generated-snippets/notification-read-with-logined-member-api-test/read-notifications-by-member-id/request-headers.adoc[] + +===== *Http Response* + +include::{snippets}/../../build/generated-snippets/notification-read-with-logined-member-api-test/read-notifications-by-member-id/http-response.adoc[] + +===== *Http Response Fields* + +include::{snippets}/../../build/generated-snippets/notification-read-with-logined-member-api-test/read-notifications-by-member-id/response-fields.adoc[] diff --git a/backend/baton/src/docs/asciidoc/NotificationUpdateApi.adoc b/backend/baton/src/docs/asciidoc/NotificationUpdateApi.adoc new file mode 100644 index 000000000..90226c48a --- /dev/null +++ b/backend/baton/src/docs/asciidoc/NotificationUpdateApi.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/notification-update-api-test/update-notification-is-read-true-by-member/http-request.adoc[] + +===== *Http Request Headers* + +include::{snippets}/../../build/generated-snippets/notification-update-api-test/update-notification-is-read-true-by-member/request-headers.adoc[] + +===== *Http Request Path Parameters* + +include::{snippets}/../../build/generated-snippets/notification-update-api-test/update-notification-is-read-true-by-member/path-parameters.adoc[] + +===== *Http Response* + +include::{snippets}/../../build/generated-snippets/notification-update-api-test/update-notification-is-read-true-by-member/http-response.adoc[] diff --git a/backend/baton/src/docs/asciidoc/OauthLogoutApi.adoc b/backend/baton/src/docs/asciidoc/OauthLogoutApi.adoc new file mode 100644 index 000000000..2cf491c42 --- /dev/null +++ b/backend/baton/src/docs/asciidoc/OauthLogoutApi.adoc @@ -0,0 +1,25 @@ +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/oauth-logout-api-test/logout/http-request.adoc[] + +===== *Http Request Headers* + +include::{snippets}/../../build/generated-snippets/oauth-logout-api-test/logout/request-headers.adoc[] + +===== *Http Response* + +include::{snippets}/../../build/generated-snippets/oauth-logout-api-test/logout/http-response.adoc[] diff --git a/backend/baton/src/docs/asciidoc/RunnerPostReadApi.adoc b/backend/baton/src/docs/asciidoc/RunnerPostReadApi.adoc index ba10d0571..6a427755c 100644 --- a/backend/baton/src/docs/asciidoc/RunnerPostReadApi.adoc +++ b/backend/baton/src/docs/asciidoc/RunnerPostReadApi.adoc @@ -32,45 +32,49 @@ include::{snippets}/../../build/generated-snippets/runner-post-read-one-api-test include::{snippets}/../../build/generated-snippets/runner-post-read-one-api-test/read-by-runner-post-id/response-fields.adoc[] -==== *러너 게시글 전체 조회 API* +==== *러너 마이페이지 게시글 조회 API* ===== *Http Request* -include::{snippets}/../../build/generated-snippets/runner-post-read-all-api-test/read-runner-posts-by-review-status/http-request.adoc[] +include::{snippets}/../../build/generated-snippets/runner-post-read-with-logined-runner-api-test/read-runner-post-by-logined-runner-and-review-status/http-request.adoc[] + +===== *Http Request Headers* + +include::{snippets}/../../build/generated-snippets/runner-post-read-with-logined-runner-api-test/read-runner-post-by-logined-runner-and-review-status/request-headers.adoc[] ===== *Http Request Query Parameters* -include::{snippets}/../../build/generated-snippets/runner-post-read-all-api-test/read-runner-posts-by-review-status/query-parameters.adoc[] +include::{snippets}/../../build/generated-snippets/runner-post-read-with-logined-runner-api-test/read-runner-post-by-logined-runner-and-review-status/query-parameters.adoc[] ===== *Http Response* -include::{snippets}/../../build/generated-snippets/runner-post-read-all-api-test/read-runner-posts-by-review-status/http-response.adoc[] +include::{snippets}/../../build/generated-snippets/runner-post-read-with-logined-runner-api-test/read-runner-post-by-logined-runner-and-review-status/http-response.adoc[] ===== *Http Response Fields* -include::{snippets}/../../build/generated-snippets/runner-post-read-all-api-test/read-runner-posts-by-review-status/response-fields.adoc[] +include::{snippets}/../../build/generated-snippets/runner-post-read-with-logined-runner-api-test/read-runner-post-by-logined-runner-and-review-status/response-fields.adoc[] -==== *러너 마이페이지 게시글 조회 API* +==== *러너 마이페이지 게시글 개수 조회 API* ===== *Http Request* -include::{snippets}/../../build/generated-snippets/runner-post-read-all-api-test/read-runner-my-page/http-request.adoc[] +include::{snippets}/../../build/generated-snippets/runner-post-count-with-logined-runner-api-test/count-runner-post-by-logined-runner-and-review-status/http-request.adoc[] ===== *Http Request Headers* -include::{snippets}/../../build/generated-snippets/runner-post-read-all-api-test/read-runner-my-page/request-headers.adoc[] +include::{snippets}/../../build/generated-snippets/runner-post-count-with-logined-runner-api-test/count-runner-post-by-logined-runner-and-review-status/request-headers.adoc[] ===== *Http Request Query Parameters* -include::{snippets}/../../build/generated-snippets/runner-post-read-all-api-test/read-runner-my-page/query-parameters.adoc[] +include::{snippets}/../../build/generated-snippets/runner-post-count-with-logined-runner-api-test/count-runner-post-by-logined-runner-and-review-status/query-parameters.adoc[] ===== *Http Response* -include::{snippets}/../../build/generated-snippets/runner-post-read-all-api-test/read-runner-my-page/http-response.adoc[] +include::{snippets}/../../build/generated-snippets/runner-post-count-with-logined-runner-api-test/count-runner-post-by-logined-runner-and-review-status/http-response.adoc[] ===== *Http Response Fields* -include::{snippets}/../../build/generated-snippets/runner-post-read-all-api-test/read-runner-my-page/response-fields.adoc[] +include::{snippets}/../../build/generated-snippets/runner-post-count-with-logined-runner-api-test/count-runner-post-by-logined-runner-and-review-status/response-fields.adoc[] ==== *리뷰 지원한 서포터 조회 API* @@ -98,37 +102,77 @@ include::{snippets}/../../build/generated-snippets/supporter-runner-post-read-ap ===== *Http Request* -include::{snippets}/../../build/generated-snippets/runner-post-read-with-logined-supporter-api-test/read-runner-posts-by-logined-supporter-and-review-status/http-request.adoc[] +include::{snippets}/../../build/generated-snippets/runner-post-read-with-logined-supporter-api-test/read-runner-post-by-logined-supporter-and-review-status/http-request.adoc[] + +===== *Http Request Query Parameters* + +include::{snippets}/../../build/generated-snippets/runner-post-read-with-logined-supporter-api-test/read-runner-post-by-logined-supporter-and-review-status/query-parameters.adoc[] + +===== *Http Response* + +include::{snippets}/../../build/generated-snippets/runner-post-read-with-logined-supporter-api-test/read-runner-post-by-logined-supporter-and-review-status/http-response.adoc[] + +===== *Http Response Fields* + +include::{snippets}/../../build/generated-snippets/runner-post-read-with-logined-supporter-api-test/read-runner-post-by-logined-supporter-and-review-status/response-fields.adoc[] + +==== *서포터 마이페이지 게시글 개수 조회 API* + +===== *Http Request* + +include::{snippets}/../../build/generated-snippets/runner-post-count-with-logined-supporter-api-test/count-runner-post-by-logined-supporter-and-review-status/http-request.adoc[] + +===== *Http Request Headers* + +include::{snippets}/../../build/generated-snippets/runner-post-count-with-logined-supporter-api-test/count-runner-post-by-logined-supporter-and-review-status/request-headers.adoc[] + +===== *Http Request Query Parameters* + +include::{snippets}/../../build/generated-snippets/runner-post-count-with-logined-supporter-api-test/count-runner-post-by-logined-supporter-and-review-status/query-parameters.adoc[] + +===== *Http Response* + +include::{snippets}/../../build/generated-snippets/runner-post-count-with-logined-supporter-api-test/count-runner-post-by-logined-supporter-and-review-status/http-response.adoc[] + +===== *Http Response Fields* + +include::{snippets}/../../build/generated-snippets/runner-post-count-with-logined-supporter-api-test/count-runner-post-by-logined-supporter-and-review-status/response-fields.adoc[] + +==== *서포터의 리뷰 완료한 게시글 조회 API* + +===== *Http Request* + +include::{snippets}/../../build/generated-snippets/runner-post-read-of-supporter-by-guest-api-test/read-runner-post-by-supporter-id-and-review-status/http-request.adoc[] ===== *Http Request Query Parameters* -include::{snippets}/../../build/generated-snippets/runner-post-read-with-logined-supporter-api-test/read-runner-posts-by-logined-supporter-and-review-status/query-parameters.adoc[] +include::{snippets}/../../build/generated-snippets/runner-post-read-of-supporter-by-guest-api-test/read-runner-post-by-supporter-id-and-review-status/query-parameters.adoc[] ===== *Http Response* -include::{snippets}/../../build/generated-snippets/runner-post-read-with-logined-supporter-api-test/read-runner-posts-by-logined-supporter-and-review-status/http-response.adoc[] +include::{snippets}/../../build/generated-snippets/runner-post-read-of-supporter-by-guest-api-test/read-runner-post-by-supporter-id-and-review-status/http-response.adoc[] ===== *Http Response Fields* -include::{snippets}/../../build/generated-snippets/runner-post-read-with-logined-supporter-api-test/read-runner-posts-by-logined-supporter-and-review-status/response-fields.adoc[] +include::{snippets}/../../build/generated-snippets/runner-post-read-of-supporter-by-guest-api-test/read-runner-post-by-supporter-id-and-review-status/response-fields.adoc[] -==== *서포터 리뷰 완료한 게시글 조회 API* +==== *서포터의 리뷰 완료한 게시글 개수 조회 API* ===== *Http Request* -include::{snippets}/../../build/generated-snippets/runner-post-read-of-supporter-by-guest-api-test/read-referenced-by-supporter/http-request.adoc[] +include::{snippets}/../../build/generated-snippets/runner-post-count-of-supporter-by-guest-api-test/count-runner-post-by-supporter-id-and-review-status/http-request.adoc[] ===== *Http Request Query Parameters* -include::{snippets}/../../build/generated-snippets/runner-post-read-of-supporter-by-guest-api-test/read-referenced-by-supporter/query-parameters.adoc[] +include::{snippets}/../../build/generated-snippets/runner-post-count-of-supporter-by-guest-api-test/count-runner-post-by-supporter-id-and-review-status/query-parameters.adoc[] ===== *Http Response* -include::{snippets}/../../build/generated-snippets/runner-post-read-of-supporter-by-guest-api-test/read-referenced-by-supporter/http-response.adoc[] +include::{snippets}/../../build/generated-snippets/runner-post-count-of-supporter-by-guest-api-test/count-runner-post-by-supporter-id-and-review-status/http-response.adoc[] ===== *Http Response Fields* -include::{snippets}/../../build/generated-snippets/runner-post-read-of-supporter-by-guest-api-test/read-referenced-by-supporter/response-fields.adoc[] +include::{snippets}/../../build/generated-snippets/runner-post-count-of-supporter-by-guest-api-test/count-runner-post-by-supporter-id-and-review-status/response-fields.adoc[] ==== *태그 이름과 리뷰 상태를 조건으로 러너 게시글 페이징 조회 API* @@ -152,16 +196,16 @@ include::{snippets}/../../build/generated-snippets/runner-post-read-search-api-t ===== *Http Request* -include::{snippets}/../../build/generated-snippets/tag-read-api-test/read-tags/http-request.adoc[] +include::{snippets}/../../build/generated-snippets/tag-read-api-test/read-tags-by-reduced-name/http-request.adoc[] ===== *Http Request Query Parameters* -include::{snippets}/../../build/generated-snippets/tag-read-api-test/read-tags/query-parameters.adoc[] +include::{snippets}/../../build/generated-snippets/tag-read-api-test/read-tags-by-reduced-name/query-parameters.adoc[] ===== *Http Response* -include::{snippets}/../../build/generated-snippets/tag-read-api-test/read-tags/http-response.adoc[] +include::{snippets}/../../build/generated-snippets/tag-read-api-test/read-tags-by-reduced-name/http-response.adoc[] ===== *Http Response Fields* -include::{snippets}/../../build/generated-snippets/tag-read-api-test/read-tags/response-fields.adoc[] +include::{snippets}/../../build/generated-snippets/tag-read-api-test/read-tags-by-reduced-name/response-fields.adoc[] diff --git a/backend/baton/src/docs/asciidoc/RunnerReadApi.adoc b/backend/baton/src/docs/asciidoc/RunnerReadApi.adoc index 0a57d96bb..2a2619fb6 100644 --- a/backend/baton/src/docs/asciidoc/RunnerReadApi.adoc +++ b/backend/baton/src/docs/asciidoc/RunnerReadApi.adoc @@ -14,13 +14,13 @@ endif::[] ===== *Http Request* -include::{snippets}/../../build/generated-snippets/runner-read-by-runner-id-api-test/read-runner-profile/http-request.adoc[] +include::{snippets}/../../build/generated-snippets/runner-read-by-guest-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[] +include::{snippets}/../../build/generated-snippets/runner-read-by-guest-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[] +include::{snippets}/../../build/generated-snippets/runner-read-by-guest-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 fc596cefa..ce5430092 100644 --- a/backend/baton/src/docs/asciidoc/index.adoc +++ b/backend/baton/src/docs/asciidoc/index.adoc @@ -19,6 +19,7 @@ include::GithubBranchCreateApi.adoc[] == *[ 로그인 ]* include::GithubOauthApi.adoc[] +include::OauthLogoutApi.adoc[] include::RefreshTokenApi.adoc[] == *[ 프로필 ]* @@ -63,3 +64,17 @@ include::RunnerPostUpdateApplicantCancelationApi.adoc[] === *러너 게시글 삭제* include::RunnerPostDeleteApi.adoc[] + +== *[ 알림 ]* + +=== *알림 조회* + +include::NotificationLoginReadApi.adoc[] + +=== *알림 수정* + +include::NotificationUpdateApi.adoc[] + +=== *알림 삭제* + +include::NotificationDeleteApi.adoc[] diff --git a/backend/baton/src/main/java/touch/baton/common/schedule/ScheduleRunnerPostRepository.java b/backend/baton/src/main/java/touch/baton/common/schedule/ScheduleRunnerPostRepository.java index e877b034a..fc4007d90 100644 --- a/backend/baton/src/main/java/touch/baton/common/schedule/ScheduleRunnerPostRepository.java +++ b/backend/baton/src/main/java/touch/baton/common/schedule/ScheduleRunnerPostRepository.java @@ -3,7 +3,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; -import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.command.RunnerPost; public interface ScheduleRunnerPostRepository extends JpaRepository { diff --git a/backend/baton/src/main/java/touch/baton/config/ArgumentResolverConfig.java b/backend/baton/src/main/java/touch/baton/config/ArgumentResolverConfig.java index 4173249d0..a8bbe56fe 100644 --- a/backend/baton/src/main/java/touch/baton/config/ArgumentResolverConfig.java +++ b/backend/baton/src/main/java/touch/baton/config/ArgumentResolverConfig.java @@ -3,9 +3,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import touch.baton.domain.oauth.controller.resolver.AuthMemberPrincipalArgumentResolver; -import touch.baton.domain.oauth.controller.resolver.AuthRunnerPrincipalArgumentResolver; -import touch.baton.domain.oauth.controller.resolver.AuthSupporterPrincipalArgumentResolver; +import touch.baton.domain.oauth.query.controller.resolver.AuthMemberPrincipalArgumentResolver; +import touch.baton.domain.oauth.query.controller.resolver.AuthRunnerPrincipalArgumentResolver; +import touch.baton.domain.oauth.query.controller.resolver.AuthSupporterPrincipalArgumentResolver; import java.util.List; diff --git a/backend/baton/src/main/java/touch/baton/config/QuerydslConfig.java b/backend/baton/src/main/java/touch/baton/config/QuerydslConfig.java new file mode 100644 index 000000000..7331d9d88 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/config/QuerydslConfig.java @@ -0,0 +1,19 @@ +package touch.baton.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QuerydslConfig { + + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/backend/baton/src/main/java/touch/baton/config/WebMvcConfig.java b/backend/baton/src/main/java/touch/baton/config/WebMvcConfig.java index 0b2e32481..0bba42c5b 100644 --- a/backend/baton/src/main/java/touch/baton/config/WebMvcConfig.java +++ b/backend/baton/src/main/java/touch/baton/config/WebMvcConfig.java @@ -9,12 +9,7 @@ import static org.springframework.http.HttpHeaders.AUTHORIZATION; import static org.springframework.http.HttpHeaders.LOCATION; -import static org.springframework.http.HttpMethod.DELETE; -import static org.springframework.http.HttpMethod.GET; -import static org.springframework.http.HttpMethod.OPTIONS; -import static org.springframework.http.HttpMethod.PATCH; -import static org.springframework.http.HttpMethod.POST; -import static org.springframework.http.HttpMethod.PUT; +import static org.springframework.http.HttpMethod.*; @Configuration public class WebMvcConfig implements WebMvcConfigurer { diff --git a/backend/baton/src/main/java/touch/baton/config/converter/OauthTypeConverter.java b/backend/baton/src/main/java/touch/baton/config/converter/OauthTypeConverter.java index be414a79f..15e81b805 100644 --- a/backend/baton/src/main/java/touch/baton/config/converter/OauthTypeConverter.java +++ b/backend/baton/src/main/java/touch/baton/config/converter/OauthTypeConverter.java @@ -1,7 +1,7 @@ package touch.baton.config.converter; import org.springframework.core.convert.converter.Converter; -import touch.baton.domain.oauth.OauthType; +import touch.baton.domain.oauth.command.OauthType; public class OauthTypeConverter implements Converter { diff --git a/backend/baton/src/main/java/touch/baton/config/converter/ReviewStatusConverter.java b/backend/baton/src/main/java/touch/baton/config/converter/ReviewStatusConverter.java index 36cdfcff3..3fa245b0a 100644 --- a/backend/baton/src/main/java/touch/baton/config/converter/ReviewStatusConverter.java +++ b/backend/baton/src/main/java/touch/baton/config/converter/ReviewStatusConverter.java @@ -1,7 +1,7 @@ package touch.baton.config.converter; import org.springframework.core.convert.converter.Converter; -import touch.baton.domain.runnerpost.vo.ReviewStatus; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; public class ReviewStatusConverter implements Converter { diff --git a/backend/baton/src/main/java/touch/baton/config/filter/FilterConfig.java b/backend/baton/src/main/java/touch/baton/config/filter/FilterConfig.java index f40bb5064..4328e9c8f 100644 --- a/backend/baton/src/main/java/touch/baton/config/filter/FilterConfig.java +++ b/backend/baton/src/main/java/touch/baton/config/filter/FilterConfig.java @@ -15,7 +15,7 @@ public class FilterConfig implements WebMvcConfigurer { public FilterRegistrationBean getFilterRegistrationBean() { final FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(new MDCLoggingFilter()); registrationBean.setOrder(Integer.MIN_VALUE); - registrationBean.setUrlPatterns(List.of("/api/**")); + registrationBean.setUrlPatterns(List.of("/api/*")); return registrationBean; } } diff --git a/backend/baton/src/main/java/touch/baton/config/filter/MDCLoggingFilter.java b/backend/baton/src/main/java/touch/baton/config/filter/MDCLoggingFilter.java index e7d46b854..7e287fbe5 100644 --- a/backend/baton/src/main/java/touch/baton/config/filter/MDCLoggingFilter.java +++ b/backend/baton/src/main/java/touch/baton/config/filter/MDCLoggingFilter.java @@ -5,6 +5,8 @@ import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.core.config.Order; import org.slf4j.MDC; import org.springframework.core.Ordered; @@ -17,8 +19,9 @@ class MDCLoggingFilter implements Filter { @Override public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { - final UUID uuid = UUID.randomUUID(); - MDC.put("request_id", uuid.toString()); + final String requestId = ((HttpServletRequest) servletRequest).getHeader("X-Request-ID"); + + MDC.put("request_id", StringUtils.defaultString(requestId, UUID.randomUUID().toString())); filterChain.doFilter(servletRequest, servletResponse); MDC.clear(); } diff --git a/backend/baton/src/main/java/touch/baton/domain/common/TruncatedBaseEntity.java b/backend/baton/src/main/java/touch/baton/domain/common/TruncatedBaseEntity.java new file mode 100644 index 000000000..205b807ef --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/TruncatedBaseEntity.java @@ -0,0 +1,22 @@ +package touch.baton.domain.common; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; + +public abstract class TruncatedBaseEntity extends BaseEntity { + + @Override + public LocalDateTime getCreatedAt() { + return super.getCreatedAt().truncatedTo(ChronoUnit.MINUTES); + } + + @Override + public LocalDateTime getDeletedAt() { + return super.getDeletedAt().truncatedTo(ChronoUnit.MINUTES); + } + + @Override + public LocalDateTime getUpdatedAt() { + return super.getUpdatedAt().truncatedTo(ChronoUnit.MINUTES); + } +} 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 1ab1a0a92..06e6bdb53 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 @@ -17,6 +17,7 @@ public enum ClientErrorCode { 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", "참고 사항을 입력해주세요."), + INVALID_QUERY_STRING_FORMAT(HttpStatus.BAD_REQUEST, "RP014", "잘못된 Query Parameter 형식입니다."), REVIEW_TYPE_IS_NULL(HttpStatus.BAD_REQUEST, "FB001", "만족도를 입력해주세요."), SUPPORTER_ID_IS_NULL(HttpStatus.BAD_REQUEST, "FB002", "서포터 식별자를 입력해주세요."), diff --git a/backend/baton/src/main/java/touch/baton/domain/common/request/PageParams.java b/backend/baton/src/main/java/touch/baton/domain/common/request/PageParams.java new file mode 100644 index 000000000..07446ed03 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/request/PageParams.java @@ -0,0 +1,14 @@ +package touch.baton.domain.common.request; + +import touch.baton.domain.common.exception.validator.ValidNotNull; + +import static touch.baton.domain.common.exception.ClientErrorCode.INVALID_QUERY_STRING_FORMAT; + +public record PageParams(Long cursor, @ValidNotNull(clientErrorCode = INVALID_QUERY_STRING_FORMAT) Integer limit) { + + private static final int ADDITIONAL_QUERY_DATA_COUNT = 1; + + public Integer getLimitForQuery() { + return limit + ADDITIONAL_QUERY_DATA_COUNT; + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/response/IdExtractable.java b/backend/baton/src/main/java/touch/baton/domain/common/response/IdExtractable.java new file mode 100644 index 000000000..12e320b3d --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/response/IdExtractable.java @@ -0,0 +1,6 @@ +package touch.baton.domain.common.response; + +public interface IdExtractable { + + Long extractId(); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/response/PageResponse.java b/backend/baton/src/main/java/touch/baton/domain/common/response/PageResponse.java index 7f0a1f5d6..74df1330c 100644 --- a/backend/baton/src/main/java/touch/baton/domain/common/response/PageResponse.java +++ b/backend/baton/src/main/java/touch/baton/domain/common/response/PageResponse.java @@ -1,38 +1,37 @@ package touch.baton.domain.common.response; -import org.springframework.data.domain.Page; +import touch.baton.domain.common.request.PageParams; import java.util.List; -public record PageResponse(List data, - PageInfo pageInfo -) { +public record PageResponse(List data, PageInfo pageInfo) { - public static PageResponse from(final Page page) { - return new PageResponse<>(page.getContent(), PageInfo.from(page)); + public static PageResponse of(final List responses, final PageParams pageParams) { + final int limit = pageParams.limit(); + if (isLastPage(responses, limit)) { + return new PageResponse<>(responses, PageInfo.last()); + } + final List limitResponses = responses.subList(0, pageParams.getLimitForQuery()); + return new PageResponse<>(limitResponses, PageInfo.normal(getLastElementId(limitResponses))); } - public record PageInfo(boolean isFirst, - boolean isLast, - boolean hasNext, - int totalPages, - long totalElements, - int currentPage, - int currentSize - ) { - - private static final int START_PAGE_NUMBER = 1; - - public static PageInfo from(final Page page) { - return new PageInfo( - page.isFirst(), - page.isLast(), - page.hasNext(), - page.getTotalPages(), - page.getTotalElements(), - page.getNumber() + START_PAGE_NUMBER, - page.getSize() - ); + public record PageInfo(boolean isLast, Long nextCursor) { + + public static PageInfo last() { + return new PageInfo(true, null); + } + + public static PageInfo normal(final Long nextCursor) { + return new PageInfo(false, nextCursor); } } + + private static boolean isLastPage(final List responses, final int limit) { + return responses.size() <= limit; + } + + private static Long getLastElementId(final List responses) { + final int lastIndex = responses.size() - 1; + return responses.get(lastIndex).extractId(); + } } diff --git a/backend/baton/src/main/java/touch/baton/domain/feedback/command/SupporterFeedback.java b/backend/baton/src/main/java/touch/baton/domain/feedback/command/SupporterFeedback.java new file mode 100644 index 000000000..7cfc4cb97 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/feedback/command/SupporterFeedback.java @@ -0,0 +1,100 @@ +package touch.baton.domain.feedback.command; + +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import touch.baton.domain.common.BaseEntity; +import touch.baton.domain.feedback.command.vo.Description; +import touch.baton.domain.feedback.command.vo.ReviewType; +import touch.baton.domain.feedback.exception.SupporterFeedbackException; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.runnerpost.command.RunnerPost; + +import java.util.Objects; + +import static jakarta.persistence.EnumType.STRING; +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class SupporterFeedback extends BaseEntity { + + @GeneratedValue(strategy = IDENTITY) + @Id + private Long id; + + @Enumerated(STRING) + @Column(nullable = false) + private ReviewType reviewType; + + @Embedded + private Description description; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "supporter_id", nullable = false, foreignKey = @ForeignKey(name = "fk_supporter_feed_back_to_supporter")) + private Supporter supporter; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "runner_id", nullable = false, foreignKey = @ForeignKey(name = "fk_supporter_feed_back_to_runner")) + private Runner runner; + + @OneToOne(fetch = LAZY) + @JoinColumn(name = "runner_post_id", nullable = false, foreignKey = @ForeignKey(name = "fk_supporter_feed_back_to_runner_post")) + private RunnerPost runnerPost; + + @Builder + private SupporterFeedback(final ReviewType reviewType, final Description description, final Supporter supporter, final Runner runner, final RunnerPost runnerPost) { + this(null, reviewType, description, supporter, runner, runnerPost); + } + + private SupporterFeedback(final Long id, final ReviewType reviewType, final Description description, final Supporter supporter, final Runner runner, final RunnerPost runnerPost) { + validateNotNull(reviewType, description, supporter, runner, runnerPost); + this.id = id; + this.reviewType = reviewType; + this.description = description; + this.supporter = supporter; + this.runner = runner; + this.runnerPost = runnerPost; + } + + private void validateNotNull(final ReviewType reviewType, + final Description description, + final Supporter supporter, + final Runner runner, + final RunnerPost runnerPost + ) { + if (Objects.isNull(reviewType)) { + throw new SupporterFeedbackException("SupporterFeedback 의 reviewType 은 null 일 수 없습니다."); + } + + if (Objects.isNull(description)) { + throw new SupporterFeedbackException("SupporterFeedback 의 description 은 null 일 수 없습니다."); + } + + if (Objects.isNull(supporter)) { + throw new SupporterFeedbackException("SupporterFeedback 의 supporter 는 null 일 수 없습니다."); + } + + if (Objects.isNull(runner)) { + throw new SupporterFeedbackException("SupporterFeedback 의 runner 는 null 일 수 없습니다."); + } + + if (Objects.isNull(runnerPost)) { + throw new SupporterFeedbackException("SupporterFeedback 의 runnerPost 는 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/feedback/command/controller/FeedbackCommandController.java b/backend/baton/src/main/java/touch/baton/domain/feedback/command/controller/FeedbackCommandController.java new file mode 100644 index 000000000..12aa3391e --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/feedback/command/controller/FeedbackCommandController.java @@ -0,0 +1,38 @@ +package touch.baton.domain.feedback.command.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 org.springframework.web.util.UriComponentsBuilder; +import touch.baton.domain.feedback.command.service.FeedbackCommandService; +import touch.baton.domain.feedback.command.service.dto.SupporterFeedBackCreateRequest; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.oauth.query.controller.resolver.AuthRunnerPrincipal; + +import java.net.URI; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/feedback") +@RestController +public class FeedbackCommandController { + + private final FeedbackCommandService feedbackCommandService; + + @PostMapping("/supporter") + public ResponseEntity createSupporterFeedback(@AuthRunnerPrincipal final Runner runner, + @Valid @RequestBody final SupporterFeedBackCreateRequest request + ) { + final Long savedId = feedbackCommandService.createSupporterFeedback(runner, request); + + final URI redirectUri = UriComponentsBuilder.fromPath("/api/v1/feedback/supporter") + .path("/{id}") + .buildAndExpand(savedId) + .toUri(); + return ResponseEntity.created(redirectUri).build(); + } + +} diff --git a/backend/baton/src/main/java/touch/baton/domain/feedback/command/repository/SupporterFeedbackCommandRepository.java b/backend/baton/src/main/java/touch/baton/domain/feedback/command/repository/SupporterFeedbackCommandRepository.java new file mode 100644 index 000000000..b03f39ca2 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/feedback/command/repository/SupporterFeedbackCommandRepository.java @@ -0,0 +1,9 @@ +package touch.baton.domain.feedback.command.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import touch.baton.domain.feedback.command.SupporterFeedback; + +public interface SupporterFeedbackCommandRepository extends JpaRepository { + + boolean existsByRunnerPostIdAndSupporterId(final Long runnerPostId, final Long supporterId); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/feedback/command/service/FeedbackCommandService.java b/backend/baton/src/main/java/touch/baton/domain/feedback/command/service/FeedbackCommandService.java new file mode 100644 index 000000000..326940adf --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/feedback/command/service/FeedbackCommandService.java @@ -0,0 +1,56 @@ +package touch.baton.domain.feedback.command.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import touch.baton.domain.feedback.command.SupporterFeedback; +import touch.baton.domain.feedback.command.repository.SupporterFeedbackCommandRepository; +import touch.baton.domain.feedback.command.service.dto.SupporterFeedBackCreateRequest; +import touch.baton.domain.feedback.command.vo.Description; +import touch.baton.domain.feedback.command.vo.ReviewType; +import touch.baton.domain.feedback.exception.FeedbackBusinessException; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.repository.SupporterCommandRepository; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.repository.RunnerPostCommandRepository; + +@RequiredArgsConstructor +@Transactional +@Service +public class FeedbackCommandService { + + private static final String DELIMITER = "|"; + + private final SupporterFeedbackCommandRepository supporterFeedbackCommandRepository; + private final RunnerPostCommandRepository runnerPostCommandRepository; + private final SupporterCommandRepository supporterCommandRepository; + + public Long createSupporterFeedback(final Runner runner, final SupporterFeedBackCreateRequest request) { + final Supporter foundSupporter = supporterCommandRepository.findById(request.supporterId()) + .orElseThrow(() -> new FeedbackBusinessException("서포터를 찾을 수 없습니다.")); + final RunnerPost foundRunnerPost = runnerPostCommandRepository.findById(request.runnerPostId()) + .orElseThrow(() -> new FeedbackBusinessException("러너 게시글을 찾을 수 없습니다.")); + + if (supporterFeedbackCommandRepository.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()))) + .runner(runner) + .supporter(foundSupporter) + .runnerPost(foundRunnerPost) + .build(); + + return supporterFeedbackCommandRepository.save(supporterFeedback).getId(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/feedback/command/service/dto/SupporterFeedBackCreateRequest.java b/backend/baton/src/main/java/touch/baton/domain/feedback/command/service/dto/SupporterFeedBackCreateRequest.java new file mode 100644 index 000000000..7d4b78547 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/feedback/command/service/dto/SupporterFeedBackCreateRequest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.feedback.command.service.dto; + +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.validator.ValidNotNull; + +import java.util.List; + +public record SupporterFeedBackCreateRequest(@ValidNotNull(clientErrorCode = ClientErrorCode.REVIEW_TYPE_IS_NULL) + String reviewType, + List descriptions, + @ValidNotNull(clientErrorCode = ClientErrorCode.SUPPORTER_ID_IS_NULL) + Long supporterId, + @ValidNotNull(clientErrorCode = ClientErrorCode.RUNNER_ID_IS_NULL) + Long runnerPostId +) { +} diff --git a/backend/baton/src/main/java/touch/baton/domain/feedback/command/vo/Description.java b/backend/baton/src/main/java/touch/baton/domain/feedback/command/vo/Description.java new file mode 100644 index 000000000..31c56d47c --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/feedback/command/vo/Description.java @@ -0,0 +1,32 @@ +package touch.baton.domain.feedback.command.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 Description { + + @Column(name = "description", nullable = true, columnDefinition = "TEXT") + private String value; + + public Description(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("Description 객체 내부에 value 는 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/feedback/command/vo/ReviewType.java b/backend/baton/src/main/java/touch/baton/domain/feedback/command/vo/ReviewType.java new file mode 100644 index 000000000..13e21bbc1 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/feedback/command/vo/ReviewType.java @@ -0,0 +1,6 @@ +package touch.baton.domain.feedback.command.vo; + +public enum ReviewType { + + GREAT, GOOD, BAD +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/Member.java b/backend/baton/src/main/java/touch/baton/domain/member/command/Member.java new file mode 100644 index 000000000..4a97ae168 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/Member.java @@ -0,0 +1,140 @@ +package touch.baton.domain.member.command; + +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import touch.baton.domain.common.BaseEntity; +import touch.baton.domain.member.command.vo.Company; +import touch.baton.domain.member.command.vo.GithubUrl; +import touch.baton.domain.member.command.vo.ImageUrl; +import touch.baton.domain.member.command.vo.MemberName; +import touch.baton.domain.member.command.vo.OauthId; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.member.exception.MemberDomainException; + +import java.util.Objects; + +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class Member extends BaseEntity { + + @GeneratedValue(strategy = IDENTITY) + @Id + private Long id; + + @Embedded + private MemberName memberName; + + @Embedded + private SocialId socialId; + + @Embedded + private OauthId oauthId; + + @Embedded + private GithubUrl githubUrl; + + @Embedded + private Company company; + + @Embedded + private ImageUrl imageUrl; + + @Builder + private Member(final MemberName memberName, + final SocialId socialId, + final OauthId oauthId, + final GithubUrl githubUrl, + final Company company, + final ImageUrl imageUrl + ) { + this(null, memberName, socialId, oauthId, githubUrl, company, imageUrl); + } + + private Member(final Long id, + final MemberName memberName, + final SocialId socialId, + final OauthId oauthId, + final GithubUrl githubUrl, + final Company company, + final ImageUrl imageUrl + ) { + validateNotNull(memberName, socialId, oauthId, githubUrl, company, imageUrl); + this.id = id; + this.memberName = memberName; + this.socialId = socialId; + this.oauthId = oauthId; + this.githubUrl = githubUrl; + this.company = company; + this.imageUrl = imageUrl; + } + + private void validateNotNull(final MemberName memberName, + final SocialId socialId, + final OauthId oauthId, + final GithubUrl githubUrl, + final Company company, + final ImageUrl imageUrl + ) { + validateMemberNameNotNull(memberName); + validateSocialIdNotNull(socialId); + validateOauthIdNotNull(oauthId); + validateGithubUrlNotNull(githubUrl); + validateCompanyNotNull(company); + validateImageUrlNotNull(imageUrl); + } + + private void validateImageUrlNotNull(final ImageUrl imageUrl) { + if (Objects.isNull(imageUrl)) { + throw new MemberDomainException("Member 의 imageUrl 은 null 일 수 없습니다."); + } + } + + private void validateCompanyNotNull(final Company company) { + if (Objects.isNull(company)) { + throw new MemberDomainException("Member 의 company 는 null 일 수 없습니다."); + } + } + + private void validateGithubUrlNotNull(final GithubUrl githubUrl) { + if (Objects.isNull(githubUrl)) { + throw new MemberDomainException("Member 의 githubUrl 은 null 일 수 없습니다."); + } + } + + private void validateOauthIdNotNull(final OauthId oauthId) { + if (Objects.isNull(oauthId)) { + throw new MemberDomainException("Member 의 oauthId 는 null 일 수 없습니다."); + } + } + + private void validateSocialIdNotNull(final SocialId socialId) { + if (Objects.isNull(socialId)) { + throw new MemberDomainException("Member 의 socialId 은 null 일 수 없습니다."); + } + } + + private void validateMemberNameNotNull(final MemberName memberName) { + if (Objects.isNull(memberName)) { + throw new MemberDomainException("Member 의 name 은 null 일 수 없습니다."); + } + } + + public void updateMemberName(final MemberName memberName) { + validateMemberNameNotNull(memberName); + this.memberName = memberName; + } + + public void updateCompany(final Company company) { + validateCompanyNotNull(company); + this.company = company; + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/Runner.java b/backend/baton/src/main/java/touch/baton/domain/member/command/Runner.java new file mode 100644 index 000000000..befd0f2c0 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/Runner.java @@ -0,0 +1,108 @@ +package touch.baton.domain.member.command; + +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.command.vo.Company; +import touch.baton.domain.member.command.vo.Introduction; +import touch.baton.domain.member.command.vo.MemberName; +import touch.baton.domain.member.exception.RunnerDomainException; +import touch.baton.domain.member.exception.SupporterDomainException; +import touch.baton.domain.technicaltag.command.RunnerTechnicalTag; +import touch.baton.domain.technicaltag.command.RunnerTechnicalTags; + +import java.util.List; +import java.util.Objects; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class Runner extends BaseEntity { + + @GeneratedValue(strategy = IDENTITY) + @Id + private Long id; + + @Embedded + private Introduction introduction; + + @OneToOne(fetch = LAZY) + @JoinColumn(name = "member_id", foreignKey = @ForeignKey(name = "fk_runner_to_member"), nullable = false) + private Member member; + + @Embedded + private RunnerTechnicalTags runnerTechnicalTags; + + @Builder + private Runner(final Member member, + final RunnerTechnicalTags runnerTechnicalTags + ) { + this(null, Introduction.getDefaultIntroduction(), member, runnerTechnicalTags); + } + + private Runner(final Long id, + final Introduction introduction, + final Member member, + final RunnerTechnicalTags runnerTechnicalTags + ) { + validateMemberNotNull(member); + this.id = id; + this.introduction = introduction; + this.member = member; + this.runnerTechnicalTags = runnerTechnicalTags; + } + + private void validateMemberNotNull(final Member member) { + if (Objects.isNull(member)) { + throw new RunnerDomainException("Runner 의 member 는 null 일 수 없습니다."); + } + } + + public void updateIntroduction(final Introduction introduction) { + validateIntroductionNotNull(introduction); + this.introduction = introduction; + } + + private void validateIntroductionNotNull(final Introduction introduction) { + if (Objects.isNull(introduction)) { + throw new SupporterDomainException("Runner 의 introduction 은 null 일 수 없습니다."); + } + } + + public void updateMemberName(final MemberName memberName) { + this.member.updateMemberName(memberName); + } + + public void updateCompany(final Company company) { + this.member.updateCompany(company); + } + + public void addAllRunnerTechnicalTags(final List runnerTechnicalTags) { + this.runnerTechnicalTags.addAll(runnerTechnicalTags); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Runner runner = (Runner) o; + return Objects.equals(id, runner.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/Supporter.java b/backend/baton/src/main/java/touch/baton/domain/member/command/Supporter.java new file mode 100644 index 000000000..dc2473e77 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/Supporter.java @@ -0,0 +1,135 @@ +package touch.baton.domain.member.command; + +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.command.vo.Company; +import touch.baton.domain.member.command.vo.Introduction; +import touch.baton.domain.member.command.vo.MemberName; +import touch.baton.domain.member.command.vo.ReviewCount; +import touch.baton.domain.member.exception.SupporterDomainException; +import touch.baton.domain.technicaltag.command.SupporterTechnicalTag; +import touch.baton.domain.technicaltag.command.SupporterTechnicalTags; + +import java.util.List; +import java.util.Objects; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class Supporter extends BaseEntity { + + @GeneratedValue(strategy = IDENTITY) + @Id + private Long id; + + @Embedded + private ReviewCount reviewCount; + + @Embedded + private Introduction introduction; + + @OneToOne(fetch = LAZY) + @JoinColumn(name = "member_id", foreignKey = @ForeignKey(name = "fk_supporter_to_member"), nullable = false) + private Member member; + + @Embedded + private SupporterTechnicalTags supporterTechnicalTags; + + @Builder + private Supporter(final ReviewCount reviewCount, + final Member member, + final SupporterTechnicalTags supporterTechnicalTags + ) { + this(null, reviewCount, Introduction.getDefaultIntroduction(), member, supporterTechnicalTags); + } + + private Supporter(final Long id, + final ReviewCount reviewCount, + final Introduction introduction, + final Member member, + final SupporterTechnicalTags supporterTechnicalTags + ) { + validateNotNull(reviewCount, member, supporterTechnicalTags); + this.id = id; + this.introduction = introduction; + this.reviewCount = reviewCount; + this.member = member; + this.supporterTechnicalTags = supporterTechnicalTags; + } + + private void validateNotNull(final ReviewCount reviewCount, + final Member member, + final SupporterTechnicalTags supporterTechnicalTags + ) { + validateReviewCountNotNull(reviewCount); + validateMemberNotNull(member); + validateSupporterTechnicalTagsNotNull(supporterTechnicalTags); + } + + private void validateReviewCountNotNull(final ReviewCount reviewCount) { + if (Objects.isNull(reviewCount)) { + throw new SupporterDomainException("Supporter 의 reviewCount 는 null 일 수 없습니다."); + } + } + + private void validateMemberNotNull(final Member member) { + if (Objects.isNull(member)) { + throw new SupporterDomainException("Supporter 의 member 는 null 일 수 없습니다."); + } + } + + private void validateSupporterTechnicalTagsNotNull(final SupporterTechnicalTags supporterTechnicalTags) { + if (Objects.isNull(supporterTechnicalTags)) { + throw new SupporterDomainException("Supporter 의 supporterTechnicalTags 는 null 일 수 없습니다."); + } + } + + public void updateIntroduction(final Introduction introduction) { + validateIntroductionNotNull(introduction); + this.introduction = introduction; + } + + private void validateIntroductionNotNull(final Introduction introduction) { + if (Objects.isNull(introduction)) { + throw new SupporterDomainException("Supporter 의 introduction 은 null 일 수 없습니다."); + } + } + + public void addAllSupporterTechnicalTags(final List supporterTechnicalTags) { + this.supporterTechnicalTags.addAll(supporterTechnicalTags); + } + + public void updateMemberName(final MemberName memberName) { + this.member.updateMemberName(memberName); + } + + public void updateCompany(final Company company) { + this.member.updateCompany(company); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Supporter supporter = (Supporter) o; + return Objects.equals(id, supporter.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/SupporterRunnerPost.java b/backend/baton/src/main/java/touch/baton/domain/member/command/SupporterRunnerPost.java new file mode 100644 index 000000000..36f3a01e1 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/SupporterRunnerPost.java @@ -0,0 +1,92 @@ +package touch.baton.domain.member.command; + +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.ManyToOne; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import touch.baton.domain.common.BaseEntity; +import touch.baton.domain.member.command.vo.Message; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.exception.RunnerPostDomainException; + +import java.util.Objects; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class SupporterRunnerPost extends BaseEntity { + + @GeneratedValue(strategy = IDENTITY) + @Id + private Long id; + + @Embedded + private Message message; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "supporter_id", + nullable = false, + foreignKey = @ForeignKey(name = "fk_support_runner_post_to_supporter")) + private Supporter supporter; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "runner_post_id", + nullable = false, + foreignKey = @ForeignKey(name = "fk_support_runner_post_to_runner_post")) + private RunnerPost runnerPost; + + @Builder + private SupporterRunnerPost(final Message message, + final Supporter supporter, + final RunnerPost runnerPost + ) { + this(null, message, supporter, runnerPost); + } + + private SupporterRunnerPost(final Long id, final Message message, final Supporter supporter, final RunnerPost runnerPost) { + validateNotNull(message, supporter, runnerPost); + this.id = id; + this.message = message; + this.supporter = supporter; + this.runnerPost = runnerPost; + } + + private void validateNotNull(final Message message, + final Supporter supporter, + final RunnerPost runnerPost + ) { + if (Objects.isNull(message)) { + throw new RunnerPostDomainException("SupporterRunnerPost 의 message 는 null 일 수 없습니다."); + } + + if (Objects.isNull(supporter)) { + throw new RunnerPostDomainException("SupporterRunnerPost 의 supporter 는 null 일 수 없습니다."); + } + + if (Objects.isNull(runnerPost)) { + throw new RunnerPostDomainException("SupporterRunnerPost 의 runnerPost 는 null 일 수 없습니다."); + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof SupporterRunnerPost that)) return false; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/controller/MemberBranchController.java b/backend/baton/src/main/java/touch/baton/domain/member/command/controller/MemberBranchController.java new file mode 100644 index 000000000..ed1170cac --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/controller/MemberBranchController.java @@ -0,0 +1,33 @@ +package touch.baton.domain.member.command.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.command.Member; +import touch.baton.domain.member.command.service.GithubBranchManageable; +import touch.baton.domain.member.command.service.dto.GithubRepoNameRequest; +import touch.baton.domain.oauth.query.controller.resolver.AuthMemberPrincipal; + +import java.net.URI; + +// FIXME: 2023/09/26 패키지 위치 변경 +@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/command/controller/RunnerCommandController.java b/backend/baton/src/main/java/touch/baton/domain/member/command/controller/RunnerCommandController.java new file mode 100644 index 000000000..75fda80fb --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/controller/RunnerCommandController.java @@ -0,0 +1,32 @@ +package touch.baton.domain.member.command.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.UriComponentsBuilder; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.service.RunnerCommandService; +import touch.baton.domain.member.command.service.dto.RunnerUpdateRequest; +import touch.baton.domain.oauth.query.controller.resolver.AuthRunnerPrincipal; + +import java.net.URI; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/profile/runner") +@RestController +public class RunnerCommandController { + + private final RunnerCommandService runnerCommandService; + + @PatchMapping("/me") + public ResponseEntity updateMyProfile(@AuthRunnerPrincipal final Runner runner, + @RequestBody @Valid final RunnerUpdateRequest runnerUpdateRequest) { + runnerCommandService.updateRunner(runner, runnerUpdateRequest); + final URI redirectUri = UriComponentsBuilder.fromPath("/api/v1/profile/runner/me").build().toUri(); + return ResponseEntity.noContent().location(redirectUri).build(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/controller/SupporterCommandController.java b/backend/baton/src/main/java/touch/baton/domain/member/command/controller/SupporterCommandController.java new file mode 100644 index 000000000..a9e471cbe --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/controller/SupporterCommandController.java @@ -0,0 +1,32 @@ +package touch.baton.domain.member.command.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.UriComponentsBuilder; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.service.SupporterCommandService; +import touch.baton.domain.member.command.service.dto.SupporterUpdateRequest; +import touch.baton.domain.oauth.query.controller.resolver.AuthSupporterPrincipal; + +import java.net.URI; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/profile/supporter") +@RestController +public class SupporterCommandController { + + private final SupporterCommandService supporterCommandService; + + @PatchMapping("/me") + public ResponseEntity updateProfile(@AuthSupporterPrincipal final Supporter supporter, + @RequestBody @Valid final SupporterUpdateRequest request) { + supporterCommandService.updateSupporter(supporter, request); + final URI redirectUri = UriComponentsBuilder.fromPath("/api/v1/profile/supporter/me").build().toUri(); + return ResponseEntity.noContent().location(redirectUri).build(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/controller/response/LoginMemberInfoResponse.java b/backend/baton/src/main/java/touch/baton/domain/member/command/controller/response/LoginMemberInfoResponse.java new file mode 100644 index 000000000..991c18a69 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/controller/response/LoginMemberInfoResponse.java @@ -0,0 +1,10 @@ +package touch.baton.domain.member.command.controller.response; + +import touch.baton.domain.member.command.Member; + +public record LoginMemberInfoResponse(String name, String imageUrl) { + + public static LoginMemberInfoResponse from(final Member member) { + return new LoginMemberInfoResponse(member.getMemberName().getValue(), member.getImageUrl().getValue()); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/repository/MemberCommandRepository.java b/backend/baton/src/main/java/touch/baton/domain/member/command/repository/MemberCommandRepository.java new file mode 100644 index 000000000..9345f3012 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/repository/MemberCommandRepository.java @@ -0,0 +1,7 @@ +package touch.baton.domain.member.command.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import touch.baton.domain.member.command.Member; + +public interface MemberCommandRepository extends JpaRepository { +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/repository/SupporterCommandRepository.java b/backend/baton/src/main/java/touch/baton/domain/member/command/repository/SupporterCommandRepository.java new file mode 100644 index 000000000..68b71b1f7 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/repository/SupporterCommandRepository.java @@ -0,0 +1,7 @@ +package touch.baton.domain.member.command.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import touch.baton.domain.member.command.Supporter; + +public interface SupporterCommandRepository extends JpaRepository { +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/repository/SupporterRunnerPostCommandRepository.java b/backend/baton/src/main/java/touch/baton/domain/member/command/repository/SupporterRunnerPostCommandRepository.java new file mode 100644 index 000000000..5400c2d9b --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/repository/SupporterRunnerPostCommandRepository.java @@ -0,0 +1,13 @@ +package touch.baton.domain.member.command.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import touch.baton.domain.member.command.SupporterRunnerPost; + +public interface SupporterRunnerPostCommandRepository extends JpaRepository { + + boolean existsByRunnerPostId(final Long runnerPostId); + + boolean existsByRunnerPostIdAndSupporterId(final Long runnerPostId, final Long supporterId); + + void deleteBySupporterIdAndRunnerPostId(final Long supporterId, final Long runnerPostId); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/service/GithubBranchManageable.java b/backend/baton/src/main/java/touch/baton/domain/member/command/service/GithubBranchManageable.java new file mode 100644 index 000000000..bac70d13a --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/service/GithubBranchManageable.java @@ -0,0 +1,6 @@ +package touch.baton.domain.member.command.service; + +public interface GithubBranchManageable { + + void createBranch(final String repoName, final String newBranchName); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/service/RunnerCommandService.java b/backend/baton/src/main/java/touch/baton/domain/member/command/service/RunnerCommandService.java new file mode 100644 index 000000000..a8ad87d24 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/service/RunnerCommandService.java @@ -0,0 +1,66 @@ +package touch.baton.domain.member.command.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.service.dto.RunnerUpdateRequest; +import touch.baton.domain.member.command.vo.Company; +import touch.baton.domain.member.command.vo.Introduction; +import touch.baton.domain.member.command.vo.MemberName; +import touch.baton.domain.technicaltag.command.RunnerTechnicalTag; +import touch.baton.domain.technicaltag.command.TechnicalTag; +import touch.baton.domain.technicaltag.command.repository.RunnerTechnicalTagCommandRepository; +import touch.baton.domain.technicaltag.query.repository.TechnicalTagQueryRepository; + +import java.util.List; + +@RequiredArgsConstructor +@Transactional +@Service +public class RunnerCommandService { + + private final RunnerTechnicalTagCommandRepository runnerTechnicalTagCommandRepository; + private final TechnicalTagQueryRepository technicalTagQueryRepository; + + public void updateRunner(Runner runner, RunnerUpdateRequest runnerUpdateRequest) { + runner.updateMemberName(new MemberName(runnerUpdateRequest.name())); + runner.updateCompany(new Company(runnerUpdateRequest.company())); + runner.updateIntroduction(new Introduction(runnerUpdateRequest.introduction())); + updateTechnicalTags(runner, runnerUpdateRequest.technicalTags()); + } + + private void updateTechnicalTags(final Runner runner, final List technicalTags) { + runnerTechnicalTagCommandRepository.deleteByRunner(runner); + createRunnerTechnicalTags(runner, technicalTags); + } + + private List createRunnerTechnicalTags(final Runner runner, final List technicalTags) { + return technicalTags.stream() + .map(tagName -> createRunnerTechnicalTag(runner, new TagName(tagName))) + .toList(); + } + + private RunnerTechnicalTag createRunnerTechnicalTag(final Runner runner, final TagName tagName) { + final TechnicalTag technicalTag = findTechnicalTagIfExistElseCreate(tagName); + return createRunnerTechnicalTagAndSave(runner, technicalTag); + } + + private TechnicalTag findTechnicalTagIfExistElseCreate(final TagName tagName) { + return technicalTagQueryRepository.findByTagName(tagName) + .orElseGet(() -> technicalTagQueryRepository.save( + TechnicalTag.builder() + .tagName(tagName) + .build()) + ); + } + + private RunnerTechnicalTag createRunnerTechnicalTagAndSave(final Runner runner, final TechnicalTag technicalTag) { + RunnerTechnicalTag runnerTechnicalTag = RunnerTechnicalTag.builder() + .runner(runner) + .technicalTag(technicalTag) + .build(); + return runnerTechnicalTagCommandRepository.save(runnerTechnicalTag); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/service/SupporterCommandService.java b/backend/baton/src/main/java/touch/baton/domain/member/command/service/SupporterCommandService.java new file mode 100644 index 000000000..f956839ed --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/service/SupporterCommandService.java @@ -0,0 +1,53 @@ +package touch.baton.domain.member.command.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.service.dto.SupporterUpdateRequest; +import touch.baton.domain.member.command.vo.Company; +import touch.baton.domain.member.command.vo.Introduction; +import touch.baton.domain.member.command.vo.MemberName; +import touch.baton.domain.technicaltag.command.SupporterTechnicalTag; +import touch.baton.domain.technicaltag.command.TechnicalTag; +import touch.baton.domain.technicaltag.command.repository.SupporterTechnicalTagCommandRepository; +import touch.baton.domain.technicaltag.query.repository.TechnicalTagQueryRepository; + +import java.util.Optional; + +@RequiredArgsConstructor +@Transactional +@Service +public class SupporterCommandService { + + private final TechnicalTagQueryRepository technicalTagQueryRepository; + private final SupporterTechnicalTagCommandRepository supporterTechnicalTagCommandRepository; + + public void updateSupporter(final Supporter supporter, final SupporterUpdateRequest supporterUpdateRequest) { + supporter.updateMemberName(new MemberName(supporterUpdateRequest.name())); + supporter.updateCompany(new Company(supporterUpdateRequest.company())); + supporter.updateIntroduction(new Introduction(supporterUpdateRequest.introduction())); + supporterTechnicalTagCommandRepository.deleteBySupporter(supporter); + supporterUpdateRequest.technicalTags() + .forEach(tagName -> createSupporterTechnicalTag(supporter, new TagName(tagName))); + } + + private SupporterTechnicalTag createSupporterTechnicalTag(final Supporter supporter, final TagName tagName) { + final TechnicalTag technicalTag = findTechnicalTagIfExistElseCreate(tagName); + return supporterTechnicalTagCommandRepository.save(SupporterTechnicalTag.builder() + .supporter(supporter) + .technicalTag(technicalTag) + .build() + ); + } + + private TechnicalTag findTechnicalTagIfExistElseCreate(final TagName tagName) { + final Optional maybeTechnicalTag = technicalTagQueryRepository.findByTagName(tagName); + return maybeTechnicalTag.orElseGet(() -> + technicalTagQueryRepository.save(TechnicalTag.builder() + .tagName(tagName) + .build() + )); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/service/dto/GithubRepoNameRequest.java b/backend/baton/src/main/java/touch/baton/domain/member/command/service/dto/GithubRepoNameRequest.java new file mode 100644 index 000000000..1ffb313df --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/service/dto/GithubRepoNameRequest.java @@ -0,0 +1,7 @@ +package touch.baton.domain.member.command.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/member/command/service/dto/RunnerUpdateRequest.java b/backend/baton/src/main/java/touch/baton/domain/member/command/service/dto/RunnerUpdateRequest.java new file mode 100644 index 000000000..702223833 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/service/dto/RunnerUpdateRequest.java @@ -0,0 +1,17 @@ +package touch.baton.domain.member.command.service.dto; + +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.validator.ValidNotNull; + +import java.util.List; + +public record RunnerUpdateRequest(@ValidNotNull(clientErrorCode = ClientErrorCode.NAME_IS_NULL) + String name, + @ValidNotNull(clientErrorCode = ClientErrorCode.COMPANY_IS_NULL) + String company, + @ValidNotNull(clientErrorCode = ClientErrorCode.RUNNER_INTRODUCTION_IS_NULL) + String introduction, + @ValidNotNull(clientErrorCode = ClientErrorCode.RUNNER_TECHNICAL_TAGS_ARE_NULL) + List technicalTags +) { +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/service/dto/SupporterUpdateRequest.java b/backend/baton/src/main/java/touch/baton/domain/member/command/service/dto/SupporterUpdateRequest.java new file mode 100644 index 000000000..39007afa7 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/service/dto/SupporterUpdateRequest.java @@ -0,0 +1,18 @@ +package touch.baton.domain.member.command.service.dto; + +import touch.baton.domain.common.exception.validator.ValidNotNull; + +import java.util.List; + +import static touch.baton.domain.common.exception.ClientErrorCode.*; + +public record SupporterUpdateRequest(@ValidNotNull(clientErrorCode = NAME_IS_NULL) + String name, + @ValidNotNull(clientErrorCode = COMPANY_IS_NULL) + String company, + @ValidNotNull(clientErrorCode = SUPPORTER_INTRODUCTION_IS_NULL) + String introduction, + @ValidNotNull(clientErrorCode = SUPPORTER_TECHNICAL_TAGS_ARE_NULL) + List technicalTags +) { +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/vo/Company.java b/backend/baton/src/main/java/touch/baton/domain/member/command/vo/Company.java new file mode 100644 index 000000000..b63ff3c56 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/vo/Company.java @@ -0,0 +1,32 @@ +package touch.baton.domain.member.command.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 Company { + + @Column(name = "company", nullable = false) + private String value; + + public Company(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("Company 객체 내부에 company 는 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/vo/GithubUrl.java b/backend/baton/src/main/java/touch/baton/domain/member/command/vo/GithubUrl.java new file mode 100644 index 000000000..cef2fa883 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/vo/GithubUrl.java @@ -0,0 +1,32 @@ +package touch.baton.domain.member.command.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 GithubUrl { + + @Column(name = "github_url", nullable = false) + private String value; + + public GithubUrl(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("GithubUrl 객체 내부에 githubUrl 은 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/vo/ImageUrl.java b/backend/baton/src/main/java/touch/baton/domain/member/command/vo/ImageUrl.java new file mode 100644 index 000000000..e389e4ac0 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/vo/ImageUrl.java @@ -0,0 +1,32 @@ +package touch.baton.domain.member.command.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 ImageUrl { + + @Column(name = "image_url", nullable = false) + private String value; + + public ImageUrl(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("ImageUrl 객체 내부에 imageUrl 은 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/vo/Introduction.java b/backend/baton/src/main/java/touch/baton/domain/member/command/vo/Introduction.java new file mode 100644 index 000000000..5bdcf7a31 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/vo/Introduction.java @@ -0,0 +1,38 @@ +package touch.baton.domain.member.command.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 Introduction { + + private static final String DEFAULT_VALUE = "안녕하세요."; + + @Column(name = "introduction", nullable = false) + private String value = DEFAULT_VALUE; + + public Introduction(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("Introduction 의 value 는 null 일 수 없습니다."); + } + } + + public static Introduction getDefaultIntroduction() { + return new Introduction(DEFAULT_VALUE); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/vo/MemberName.java b/backend/baton/src/main/java/touch/baton/domain/member/command/vo/MemberName.java new file mode 100644 index 000000000..6b7e21786 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/vo/MemberName.java @@ -0,0 +1,30 @@ +package touch.baton.domain.member.command.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 MemberName { + + private static final String DEFAULT_VALUE = "익명의 사용자"; + + @Column(name = "name", nullable = false) + private String value = DEFAULT_VALUE; + + public MemberName(final String value) { + if (Objects.isNull(value)) { + return; + } + this.value = value; + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/vo/Message.java b/backend/baton/src/main/java/touch/baton/domain/member/command/vo/Message.java new file mode 100644 index 000000000..cfa16391b --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/vo/Message.java @@ -0,0 +1,32 @@ +package touch.baton.domain.member.command.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 Message { + + @Column(name = "message", nullable = false) + private String value; + + public Message(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("Message 객체 내부에 message 는 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/vo/OauthId.java b/backend/baton/src/main/java/touch/baton/domain/member/command/vo/OauthId.java new file mode 100644 index 000000000..88437c360 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/vo/OauthId.java @@ -0,0 +1,32 @@ +package touch.baton.domain.member.command.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 OauthId { + + @Column(name = "oauth_id", nullable = false) + private String value; + + public OauthId(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("OauthId 객체 내부에 oauthId 는 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/vo/ReviewCount.java b/backend/baton/src/main/java/touch/baton/domain/member/command/vo/ReviewCount.java new file mode 100644 index 000000000..958b89132 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/vo/ReviewCount.java @@ -0,0 +1,27 @@ +package touch.baton.domain.member.command.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnDefault; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class ReviewCount { + + private static final String DEFAULT_VALUE = "0"; + + @ColumnDefault(DEFAULT_VALUE) + @Column(name = "review_count") + private int value; + + public ReviewCount(final int value) { + this.value = value; + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/command/vo/SocialId.java b/backend/baton/src/main/java/touch/baton/domain/member/command/vo/SocialId.java new file mode 100644 index 000000000..aae0a1080 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/command/vo/SocialId.java @@ -0,0 +1,32 @@ +package touch.baton.domain.member.command.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 SocialId { + + @Column(name = "social_id", nullable = false) + private String value; + + public SocialId(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("SocialId 객체 내부에 value 은 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/exception/RunnerBusinessException.java b/backend/baton/src/main/java/touch/baton/domain/member/exception/RunnerBusinessException.java new file mode 100644 index 000000000..8b8be4303 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/exception/RunnerBusinessException.java @@ -0,0 +1,10 @@ +package touch.baton.domain.member.exception; + +import touch.baton.domain.common.exception.BusinessException; + +public class RunnerBusinessException extends BusinessException { + + public RunnerBusinessException(final String message) { + super(message); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/exception/RunnerDomainException.java b/backend/baton/src/main/java/touch/baton/domain/member/exception/RunnerDomainException.java new file mode 100644 index 000000000..6d6796b05 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/exception/RunnerDomainException.java @@ -0,0 +1,10 @@ +package touch.baton.domain.member.exception; + +import touch.baton.domain.common.exception.DomainException; + +public class RunnerDomainException extends DomainException { + + public RunnerDomainException(final String message) { + super(message); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/exception/RunnerRequestException.java b/backend/baton/src/main/java/touch/baton/domain/member/exception/RunnerRequestException.java new file mode 100644 index 000000000..1e2528ae7 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/exception/RunnerRequestException.java @@ -0,0 +1,11 @@ +package touch.baton.domain.member.exception; + +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.ClientRequestException; + +public class RunnerRequestException extends ClientRequestException { + + public RunnerRequestException(final ClientErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/exception/SupporterBusinessException.java b/backend/baton/src/main/java/touch/baton/domain/member/exception/SupporterBusinessException.java new file mode 100644 index 000000000..77adddec2 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/exception/SupporterBusinessException.java @@ -0,0 +1,10 @@ +package touch.baton.domain.member.exception; + +import touch.baton.domain.common.exception.BusinessException; + +public class SupporterBusinessException extends BusinessException { + + public SupporterBusinessException(final String message) { + super(message); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/exception/SupporterDomainException.java b/backend/baton/src/main/java/touch/baton/domain/member/exception/SupporterDomainException.java new file mode 100644 index 000000000..ecde844ef --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/exception/SupporterDomainException.java @@ -0,0 +1,10 @@ +package touch.baton.domain.member.exception; + +import touch.baton.domain.common.exception.DomainException; + +public class SupporterDomainException extends DomainException { + + public SupporterDomainException(final String message) { + super(message); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/exception/SupporterRequestException.java b/backend/baton/src/main/java/touch/baton/domain/member/exception/SupporterRequestException.java new file mode 100644 index 000000000..f1d3b55a5 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/exception/SupporterRequestException.java @@ -0,0 +1,11 @@ +package touch.baton.domain.member.exception; + +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.ClientRequestException; + +public class SupporterRequestException extends ClientRequestException { + + public SupporterRequestException(final ClientErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/query/controller/MemberQueryController.java b/backend/baton/src/main/java/touch/baton/domain/member/query/controller/MemberQueryController.java new file mode 100644 index 000000000..20f6dbd0a --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/query/controller/MemberQueryController.java @@ -0,0 +1,21 @@ +package touch.baton.domain.member.query.controller; + +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.RestController; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.controller.response.LoginMemberInfoResponse; +import touch.baton.domain.oauth.query.controller.resolver.AuthMemberPrincipal; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/profile") +@RestController +public class MemberQueryController { + + @GetMapping("/me") + ResponseEntity readLoginMemberInfo(@AuthMemberPrincipal final Member member) { + return ResponseEntity.ok(LoginMemberInfoResponse.from(member)); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/query/controller/RunnerQueryController.java b/backend/baton/src/main/java/touch/baton/domain/member/query/controller/RunnerQueryController.java new file mode 100644 index 000000000..073670b57 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/query/controller/RunnerQueryController.java @@ -0,0 +1,33 @@ +package touch.baton.domain.member.query.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.query.controller.response.RunnerResponse; +import touch.baton.domain.member.query.service.RunnerQueryService; +import touch.baton.domain.oauth.query.controller.resolver.AuthRunnerPrincipal; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/profile/runner") +@RestController +public class RunnerQueryController { + + private final RunnerQueryService runnerQueryService; + + @GetMapping("/me") + public ResponseEntity readMyProfileByToken(@AuthRunnerPrincipal Runner runner) { + final RunnerResponse.Mine response = RunnerResponse.Mine.from(runner); + return ResponseEntity.ok(response); + } + + @GetMapping("/{runnerId}") + public ResponseEntity readRunnerProfile(@PathVariable Long runnerId) { + final Runner runner = runnerQueryService.readByRunnerId(runnerId); + final RunnerResponse.Detail response = RunnerResponse.Detail.from(runner); + return ResponseEntity.ok(response); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/query/controller/SupporterQueryController.java b/backend/baton/src/main/java/touch/baton/domain/member/query/controller/SupporterQueryController.java new file mode 100644 index 000000000..accb59836 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/query/controller/SupporterQueryController.java @@ -0,0 +1,35 @@ +package touch.baton.domain.member.query.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.query.controller.response.SupporterResponse; +import touch.baton.domain.member.query.service.SupporterQueryService; +import touch.baton.domain.oauth.query.controller.resolver.AuthSupporterPrincipal; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/profile/supporter") +@RestController +public class SupporterQueryController { + + private final SupporterQueryService supporterQueryService; + + @GetMapping("/{supporterId}") + public ResponseEntity readProfileBySupporterId(@PathVariable final Long supporterId) { + final Supporter foundSupporter = supporterQueryService.readBySupporterId(supporterId); + final SupporterResponse.Profile response = SupporterResponse.Profile.from(foundSupporter); + + return ResponseEntity.ok(response); + } + + @GetMapping("/me") + public ResponseEntity readSupporterMyProfileByLoginToken(@AuthSupporterPrincipal final Supporter loginedSupporter) { + final SupporterResponse.MyProfile response = SupporterResponse.MyProfile.from(loginedSupporter); + + return ResponseEntity.ok(response); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/query/controller/response/RunnerResponse.java b/backend/baton/src/main/java/touch/baton/domain/member/query/controller/response/RunnerResponse.java new file mode 100644 index 000000000..2728cfda8 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/query/controller/response/RunnerResponse.java @@ -0,0 +1,86 @@ +package touch.baton.domain.member.query.controller.response; + +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; + +import java.util.List; + +public record RunnerResponse() { + + public record Detail(Long runnerId, + String name, + String imageUrl, + String githubUrl, + String introduction, + String company, + List technicalTags + ) { + + public static Detail from(final Runner runner) { + final List tagNames = runner.getRunnerTechnicalTags().getRunnerTechnicalTags().stream() + .map(runnerTechnicalTag -> runnerTechnicalTag.getTechnicalTag().getTagName().getValue()) + .toList(); + + final Member member = runner.getMember(); + return new Detail(runner.getId(), + member.getMemberName().getValue(), + member.getImageUrl().getValue(), + member.getGithubUrl().getValue(), + runner.getIntroduction().getValue(), + member.getCompany().getValue(), + tagNames); + } + } + + public record InRunnerPostDetail(Long runnerId, + String name, + String company, + String imageUrl + ) { + + public static InRunnerPostDetail from(final Runner runner) { + return new InRunnerPostDetail( + runner.getId(), + runner.getMember().getMemberName().getValue(), + runner.getMember().getCompany().getValue(), + runner.getMember().getImageUrl().getValue() + ); + } + } + + public record Simple(String name, String imageUrl) { + + public static Simple from(final Runner runner) { + return new Simple( + runner.getMember().getMemberName().getValue(), + runner.getMember().getImageUrl().getValue() + ); + } + } + + public record Mine(String name, + String company, + String imageUrl, + String githubUrl, + String introduction, + List technicalTags + ) { + + public static Mine from(final Runner runner) { + return new Mine( + runner.getMember().getMemberName().getValue(), + runner.getMember().getCompany().getValue(), + runner.getMember().getImageUrl().getValue(), + runner.getMember().getGithubUrl().getValue(), + runner.getIntroduction().getValue(), + convertRunnerTechnicalTags(runner) + ); + } + + private static List convertRunnerTechnicalTags(final Runner runner) { + return runner.getRunnerTechnicalTags().getRunnerTechnicalTags().stream() + .map(runnerTechnicalTag -> runnerTechnicalTag.getTechnicalTag().getTagName().getValue()) + .toList(); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/query/controller/response/SupporterResponse.java b/backend/baton/src/main/java/touch/baton/domain/member/query/controller/response/SupporterResponse.java new file mode 100644 index 000000000..ed9780e49 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/query/controller/response/SupporterResponse.java @@ -0,0 +1,79 @@ +package touch.baton.domain.member.query.controller.response; + +import touch.baton.domain.member.command.Supporter; + +import java.util.List; + +public record SupporterResponse() { + + public record Detail(Long supporterId, + String name, + String company, + int reviewCount, + String githubUrl, + String introduction, + List technicalTags + ) { + + public static Detail from(final Supporter supporter) { + return new Detail( + supporter.getId(), + supporter.getMember().getMemberName().getValue(), + supporter.getMember().getCompany().getValue(), + supporter.getReviewCount().getValue(), + supporter.getMember().getGithubUrl().getValue(), + supporter.getIntroduction().getValue(), + convertToTechnicalTags(supporter) + ); + } + } + + public record Profile(Long supporterId, + String name, + String company, + String imageUrl, + String githubUrl, + String introduction, + List technicalTags + ) { + public static SupporterResponse.Profile from(final Supporter supporter) { + return new SupporterResponse.Profile( + supporter.getId(), + supporter.getMember().getMemberName().getValue(), + supporter.getMember().getCompany().getValue(), + supporter.getMember().getImageUrl().getValue(), + supporter.getMember().getGithubUrl().getValue(), + supporter.getIntroduction().getValue(), + convertToTechnicalTags(supporter) + ); + } + } + + public record MyProfile( + String name, + String imageUrl, + String githubUrl, + String introduction, + String company, + List technicalTags + ) { + + public static MyProfile from(final Supporter supporter) { + return new SupporterResponse.MyProfile( + supporter.getMember().getMemberName().getValue(), + supporter.getMember().getImageUrl().getValue(), + supporter.getMember().getGithubUrl().getValue(), + supporter.getIntroduction().getValue(), + supporter.getMember().getCompany().getValue(), + convertToTechnicalTags(supporter) + ); + } + } + + private static List convertToTechnicalTags(final Supporter supporter) { + return supporter.getSupporterTechnicalTags().getSupporterTechnicalTags() + .stream() + .map(supporterTechnicalTag -> supporterTechnicalTag.getTechnicalTag().getTagName().getValue()) + .toList(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/query/repository/RunnerQueryRepository.java b/backend/baton/src/main/java/touch/baton/domain/member/query/repository/RunnerQueryRepository.java new file mode 100644 index 000000000..3828a1a9e --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/query/repository/RunnerQueryRepository.java @@ -0,0 +1,19 @@ +package touch.baton.domain.member.query.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.member.command.Runner; + +import java.util.Optional; + +public interface RunnerQueryRepository extends JpaRepository { + + @Query(""" + select r + from Runner r + join fetch Member m on m.id = r.member.id + where r.id = :runnerId + """) + Optional joinMemberByRunnerId(@Param("runnerId") Long runnerId); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/query/repository/SupporterQueryRepository.java b/backend/baton/src/main/java/touch/baton/domain/member/query/repository/SupporterQueryRepository.java new file mode 100644 index 000000000..90e152206 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/query/repository/SupporterQueryRepository.java @@ -0,0 +1,19 @@ +package touch.baton.domain.member.query.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.member.command.Supporter; + +import java.util.Optional; + +public interface SupporterQueryRepository extends JpaRepository { + + @Query(""" + select s + from Supporter s + join fetch Member m on s.member.id = m.id + where s.id = :supporterId + """) + Optional joinMemberBySupporterId(@Param("supporterId") final Long supporterId); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/query/repository/SupporterRunnerPostQueryRepository.java b/backend/baton/src/main/java/touch/baton/domain/member/query/repository/SupporterRunnerPostQueryRepository.java new file mode 100644 index 000000000..171f19605 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/query/repository/SupporterRunnerPostQueryRepository.java @@ -0,0 +1,39 @@ +package touch.baton.domain.member.query.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.member.command.SupporterRunnerPost; + +import java.util.List; +import java.util.Optional; + +public interface SupporterRunnerPostQueryRepository extends JpaRepository { + + @Query(""" + select count(1) + from SupporterRunnerPost srp + group by srp.runnerPost.id + having srp.runnerPost.id = :runnerPostId + """) + Optional countByRunnerPostId(@Param("runnerPostId") final Long runnerPostId); + + @Query(""" + select (count(1) >= 1) + from SupporterRunnerPost srp + join fetch Member m on m.id = srp.supporter.member.id + where srp.runnerPost.id = :runnerPostId + and srp.supporter.member.id = :memberId + """) + boolean existsByRunnerPostIdAndMemberId(@Param("runnerPostId") final Long runnerPostId, @Param("memberId") final Long memberId); + + List readByRunnerPostId(final Long runnerPostId); + + @Query(""" + select count(1) + from SupporterRunnerPost srp + join fetch RunnerPost rp on rp.id = srp.runnerPost.id + where rp.reviewStatus = 'NOT_STARTED' and srp.supporter.id = :supporterId + """) + long countRunnerPostBySupporterIdByReviewStatusNotStarted(@Param("supporterId") final Long supporterId); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/query/service/RunnerQueryService.java b/backend/baton/src/main/java/touch/baton/domain/member/query/service/RunnerQueryService.java new file mode 100644 index 000000000..6697bfc49 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/query/service/RunnerQueryService.java @@ -0,0 +1,21 @@ +package touch.baton.domain.member.query.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.exception.RunnerBusinessException; +import touch.baton.domain.member.query.repository.RunnerQueryRepository; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class RunnerQueryService { + + private final RunnerQueryRepository runnerQueryRepository; + + public Runner readByRunnerId(final Long runnerId) { + return runnerQueryRepository.joinMemberByRunnerId(runnerId) + .orElseThrow(() -> new RunnerBusinessException("Runner 가 존재하지 않습니다.")); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/query/service/SupporterQueryService.java b/backend/baton/src/main/java/touch/baton/domain/member/query/service/SupporterQueryService.java new file mode 100644 index 000000000..7345f150e --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/query/service/SupporterQueryService.java @@ -0,0 +1,21 @@ +package touch.baton.domain.member.query.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.exception.SupporterBusinessException; +import touch.baton.domain.member.query.repository.SupporterQueryRepository; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class SupporterQueryService { + + private final SupporterQueryRepository supporterQueryRepository; + + public Supporter readBySupporterId(final Long supporterId) { + return supporterQueryRepository.joinMemberBySupporterId(supporterId) + .orElseThrow(() -> new SupporterBusinessException("존재하지 않는 서포터 식별자값으로 조회할 수 없습니다.")); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/notification/command/Notification.java b/backend/baton/src/main/java/touch/baton/domain/notification/command/Notification.java new file mode 100644 index 000000000..3528cad21 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/notification/command/Notification.java @@ -0,0 +1,146 @@ +package touch.baton.domain.notification.command; + +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; +import touch.baton.domain.common.TruncatedBaseEntity; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.notification.command.vo.IsRead; +import touch.baton.domain.notification.command.vo.NotificationMessage; +import touch.baton.domain.notification.command.vo.NotificationReferencedId; +import touch.baton.domain.notification.command.vo.NotificationTitle; +import touch.baton.domain.notification.command.vo.NotificationType; +import touch.baton.domain.notification.exception.NotificationDomainException; + +import java.util.Objects; + +import static jakarta.persistence.EnumType.STRING; +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Where(clause = "deleted_at IS NULL") +@SQLDelete(sql = "UPDATE notification SET deleted_at = now() WHERE id = ?") +@Entity +public class Notification extends TruncatedBaseEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + @Embedded + private NotificationTitle notificationTitle; + + @Embedded + private NotificationMessage notificationMessage; + + @Enumerated(STRING) + @Column(nullable = false) + private NotificationType notificationType; + + @Embedded + private NotificationReferencedId notificationReferencedId; + + @Embedded + private IsRead isRead; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "member_id", + nullable = false, + foreignKey = @ForeignKey(name = "fk_notification_to_member")) + private Member member; + + @Builder + private Notification(final NotificationTitle notificationTitle, + final NotificationMessage notificationMessage, + final NotificationType notificationType, + final NotificationReferencedId notificationReferencedId, + final IsRead isRead, + final Member member + ) { + this(null, notificationTitle, notificationMessage, notificationType, notificationReferencedId, isRead, member); + } + + private Notification(final Long id, + final NotificationTitle notificationTitle, + final NotificationMessage notificationMessage, + final NotificationType notificationType, + final NotificationReferencedId notificationReferencedId, + final IsRead isRead, + final Member member + ) { + validateNotNull(notificationTitle, notificationMessage, notificationType, notificationReferencedId, isRead, member); + this.id = id; + this.notificationTitle = notificationTitle; + this.notificationMessage = notificationMessage; + this.notificationType = notificationType; + this.notificationReferencedId = notificationReferencedId; + this.isRead = isRead; + this.member = member; + } + + private void validateNotNull(final NotificationTitle notificationTitle, + final NotificationMessage notificationMessage, + final NotificationType notificationType, + final NotificationReferencedId notificationReferencedId, + final IsRead isRead, + final Member member + ) { + if (notificationTitle == null) { + throw new NotificationDomainException("NotificationTitle 의 notificationTitle 은 null 일 수 없습니다."); + } + if (notificationMessage == null) { + throw new NotificationDomainException("NotificationMessage 의 notificationMessage 는 null 일 수 없습니다."); + } + if (notificationType == null) { + throw new NotificationDomainException("NotificationType 의 notificationType 는 null 일 수 없습니다."); + } + if (notificationReferencedId == null) { + throw new NotificationDomainException("NotificationReferencedId 의 notificationReferencedId 은 null 일 수 없습니다."); + } + if (isRead == null) { + throw new NotificationDomainException("IsRead 의 isRead 는 null 일 수 없습니다."); + } + if (member == null) { + throw new NotificationDomainException("Member 의 member 는 null 일 수 없습니다."); + } + } + + public void markAsRead(final Member currentMember) { + if (!this.member.equals(currentMember)) { + throw new NotificationDomainException("Notification 의 주인(사용자)가 아니므로 알림의 읽은 여부를 수정할 수 없습니다."); + } + + this.isRead = IsRead.asRead(); + } + + public boolean isNotOwner(final Member currentMember) { + return !this.member.equals(currentMember); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Notification notification = (Notification) o; + return Objects.equals(id, notification.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/notification/command/controller/NotificationCommandController.java b/backend/baton/src/main/java/touch/baton/domain/notification/command/controller/NotificationCommandController.java new file mode 100644 index 000000000..488cfc99d --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/notification/command/controller/NotificationCommandController.java @@ -0,0 +1,38 @@ +package touch.baton.domain.notification.command.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import touch.baton.domain.notification.command.service.NotificationCommandService; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.oauth.query.controller.resolver.AuthMemberPrincipal; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/notifications") +@RestController +public class NotificationCommandController { + + private final NotificationCommandService notificationCommandService; + + @PatchMapping("/{notificationId}") + public ResponseEntity updateNotificationIsReadTrueByNotificationId(@AuthMemberPrincipal final Member member, + @PathVariable final Long notificationId + ) { + notificationCommandService.updateNotificationIsReadTrueByMember(member, notificationId); + + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/{notificationId}") + public ResponseEntity deleteNotificationByNotificationId(@AuthMemberPrincipal final Member member, + @PathVariable final Long notificationId + ) { + notificationCommandService.deleteNotificationByMember(member, notificationId); + + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/notification/command/event/NotificationEventListener.java b/backend/baton/src/main/java/touch/baton/domain/notification/command/event/NotificationEventListener.java new file mode 100644 index 000000000..e7d27e42f --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/notification/command/event/NotificationEventListener.java @@ -0,0 +1,82 @@ +package touch.baton.domain.notification.command.event; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.notification.command.Notification; +import touch.baton.domain.notification.command.repository.NotificationCommandRepository; +import touch.baton.domain.notification.command.vo.IsRead; +import touch.baton.domain.notification.command.vo.NotificationMessage; +import touch.baton.domain.notification.command.vo.NotificationReferencedId; +import touch.baton.domain.notification.command.vo.NotificationTitle; +import touch.baton.domain.notification.command.vo.NotificationType; +import touch.baton.domain.notification.exception.NotificationBusinessException; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.event.RunnerPostApplySupporterEvent; +import touch.baton.domain.runnerpost.command.event.RunnerPostAssignSupporterEvent; +import touch.baton.domain.runnerpost.command.event.RunnerPostReviewStatusDoneEvent; +import touch.baton.domain.runnerpost.query.repository.RunnerPostQueryRepository; + +import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW; +import static org.springframework.transaction.event.TransactionPhase.AFTER_COMMIT; + +@RequiredArgsConstructor +@Component +public class NotificationEventListener { + + private final NotificationCommandRepository notificationCommandRepository; + private final RunnerPostQueryRepository runnerPostQueryRepository; + + @TransactionalEventListener(phase = AFTER_COMMIT) + @Transactional(propagation = REQUIRES_NEW) + public void subscribeRunnerPostApplySupporterEvent(final RunnerPostApplySupporterEvent event) { + final RunnerPost foundRunnerPost = getRunnerPostWithRunnerOrThrowException(event.runnerPostId()); + + notificationCommandRepository.save( + createNotification("서포터의 제안이 왔습니다.", foundRunnerPost, foundRunnerPost.getRunner().getMember()) + ); + } + + private RunnerPost getRunnerPostWithRunnerOrThrowException(final Long runnerPostId) { + return runnerPostQueryRepository.joinMemberByRunnerPostId(runnerPostId) + .orElseThrow(() -> new NotificationBusinessException("러너 게시글 식별자값으로 러너 게시글과 러너(작성자)를 조회하던 도중에 오류가 발생하였습니다.")); + } + + private Notification createNotification(final String notificationTitle, final RunnerPost runnerPost, final Member targetMember) { + return Notification.builder() + .notificationTitle(new NotificationTitle(notificationTitle)) + .notificationMessage(new NotificationMessage(String.format("관련 게시글 - %s", runnerPost.getTitle().getValue()))) + .notificationType(NotificationType.RUNNER_POST) + .notificationReferencedId(new NotificationReferencedId(runnerPost.getId())) + .isRead(IsRead.asUnRead()) + .member(targetMember) + .build(); + } + + @TransactionalEventListener(phase = AFTER_COMMIT) + @Transactional(propagation = REQUIRES_NEW) + public void subscribeRunnerPostReviewStatusDoneEvent(final RunnerPostReviewStatusDoneEvent event) { + final RunnerPost foundRunnerPost = getRunnerPostWithRunnerOrThrowException(event.runnerPostId()); + + notificationCommandRepository.save( + createNotification("코드 리뷰 상태가 완료로 변경되었습니다.", foundRunnerPost, foundRunnerPost.getRunner().getMember()) + ); + } + + @TransactionalEventListener(phase = AFTER_COMMIT) + @Transactional(propagation = REQUIRES_NEW) + public void subscribeRunnerPostAssignSupporterEvent(final RunnerPostAssignSupporterEvent event) { + final RunnerPost foundRunnerPost = getRunnerPostWithSupporterOrThrowException(event.runnerPostId()); + + notificationCommandRepository.save( + createNotification("코드 리뷰 매칭이 완료되었습니다.", foundRunnerPost, foundRunnerPost.getSupporter().getMember()) + ); + } + + private RunnerPost getRunnerPostWithSupporterOrThrowException(final Long runnerPostId) { + return runnerPostQueryRepository.joinSupporterByRunnerPostId(runnerPostId) + .orElseThrow(() -> new NotificationBusinessException("러너 게시글 식별자값으로 러너 게시글과 서포터(지원자)를 조회하던 도중에 오류가 발생하였습니다.")); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/notification/command/repository/NotificationCommandRepository.java b/backend/baton/src/main/java/touch/baton/domain/notification/command/repository/NotificationCommandRepository.java new file mode 100644 index 000000000..f0b32bc6e --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/notification/command/repository/NotificationCommandRepository.java @@ -0,0 +1,7 @@ +package touch.baton.domain.notification.command.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import touch.baton.domain.notification.command.Notification; + +public interface NotificationCommandRepository extends JpaRepository { +} diff --git a/backend/baton/src/main/java/touch/baton/domain/notification/command/service/NotificationCommandService.java b/backend/baton/src/main/java/touch/baton/domain/notification/command/service/NotificationCommandService.java new file mode 100644 index 000000000..c3551428c --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/notification/command/service/NotificationCommandService.java @@ -0,0 +1,35 @@ +package touch.baton.domain.notification.command.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import touch.baton.domain.notification.command.Notification; +import touch.baton.domain.notification.command.repository.NotificationCommandRepository; +import touch.baton.domain.notification.exception.NotificationBusinessException; +import touch.baton.domain.member.command.Member; + +@RequiredArgsConstructor +@Transactional +@Service +public class NotificationCommandService { + + private final NotificationCommandRepository notificationCommandRepository; + + public void updateNotificationIsReadTrueByMember(final Member member, final Long notificationId) { + final Notification foundNotification = getNotificationByNotificationId(notificationId); + foundNotification.markAsRead(member); + } + + private Notification getNotificationByNotificationId(final Long notificationId) { + return notificationCommandRepository.findById(notificationId) + .orElseThrow(() -> new NotificationBusinessException("Notification 식별자값으로 알림을 조회할 수 없습니다.")); + } + + public void deleteNotificationByMember(final Member member, final Long notificationId) { + if (getNotificationByNotificationId(notificationId).isNotOwner(member)) { + throw new NotificationBusinessException("Notification 의 주인(사용자)가 아니므로 알림을 삭제할 수 없습니다."); + } + + notificationCommandRepository.deleteById(notificationId); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/notification/command/vo/IsRead.java b/backend/baton/src/main/java/touch/baton/domain/notification/command/vo/IsRead.java new file mode 100644 index 000000000..20fe24a13 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/notification/command/vo/IsRead.java @@ -0,0 +1,35 @@ +package touch.baton.domain.notification.command.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 IsRead { + + @ColumnDefault(value = "false") + @Column(name = "is_read", nullable = false) + private boolean value = false; + + private IsRead(final boolean value) { + this.value = value; + } + + public static IsRead asRead() { + return new IsRead(true); + } + + public static IsRead asUnRead() { + return new IsRead(false); + } + + public boolean getValue() { + return value; + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/notification/command/vo/NotificationMessage.java b/backend/baton/src/main/java/touch/baton/domain/notification/command/vo/NotificationMessage.java new file mode 100644 index 000000000..6181bf570 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/notification/command/vo/NotificationMessage.java @@ -0,0 +1,32 @@ +package touch.baton.domain.notification.command.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 NotificationMessage { + + @Column(name = "message", nullable = false) + private String value; + + public NotificationMessage(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("NotificationMessage 객체 내부에 message 는 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/notification/command/vo/NotificationReferencedId.java b/backend/baton/src/main/java/touch/baton/domain/notification/command/vo/NotificationReferencedId.java new file mode 100644 index 000000000..1892a01fe --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/notification/command/vo/NotificationReferencedId.java @@ -0,0 +1,30 @@ +package touch.baton.domain.notification.command.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class NotificationReferencedId { + + @Column(name = "referenced_id", nullable = false) + private Long value; + + public NotificationReferencedId(final Long value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final Long value) { + if (value == null) { + throw new IllegalArgumentException("NotificationReferencedId 객체 내부에 referencedId 은 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/notification/command/vo/NotificationTitle.java b/backend/baton/src/main/java/touch/baton/domain/notification/command/vo/NotificationTitle.java new file mode 100644 index 000000000..b49bd80a6 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/notification/command/vo/NotificationTitle.java @@ -0,0 +1,32 @@ +package touch.baton.domain.notification.command.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 NotificationTitle { + + @Column(name = "title", nullable = false) + private String value; + + public NotificationTitle(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("NotificationTitle 객체 내부에 title 은 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/notification/command/vo/NotificationType.java b/backend/baton/src/main/java/touch/baton/domain/notification/command/vo/NotificationType.java new file mode 100644 index 000000000..14a768acb --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/notification/command/vo/NotificationType.java @@ -0,0 +1,6 @@ +package touch.baton.domain.notification.command.vo; + +public enum NotificationType { + + RUNNER_POST; +} diff --git a/backend/baton/src/main/java/touch/baton/domain/notification/exception/NotificationBusinessException.java b/backend/baton/src/main/java/touch/baton/domain/notification/exception/NotificationBusinessException.java new file mode 100644 index 000000000..3e9a6af08 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/notification/exception/NotificationBusinessException.java @@ -0,0 +1,10 @@ +package touch.baton.domain.notification.exception; + +import touch.baton.domain.common.exception.BusinessException; + +public class NotificationBusinessException extends BusinessException { + + public NotificationBusinessException(final String message) { + super(message); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/notification/exception/NotificationDomainException.java b/backend/baton/src/main/java/touch/baton/domain/notification/exception/NotificationDomainException.java new file mode 100644 index 000000000..18d66ce9b --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/notification/exception/NotificationDomainException.java @@ -0,0 +1,10 @@ +package touch.baton.domain.notification.exception; + +import touch.baton.domain.common.exception.DomainException; + +public class NotificationDomainException extends DomainException { + + public NotificationDomainException(final String message) { + super(message); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/notification/exception/NotificationRequestException.java b/backend/baton/src/main/java/touch/baton/domain/notification/exception/NotificationRequestException.java new file mode 100644 index 000000000..16ac4d398 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/notification/exception/NotificationRequestException.java @@ -0,0 +1,11 @@ +package touch.baton.domain.notification.exception; + +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.ClientRequestException; + +public class NotificationRequestException extends ClientRequestException { + + public NotificationRequestException(final ClientErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/notification/query/controller/NotificationQueryController.java b/backend/baton/src/main/java/touch/baton/domain/notification/query/controller/NotificationQueryController.java new file mode 100644 index 000000000..fee64a9f7 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/notification/query/controller/NotificationQueryController.java @@ -0,0 +1,31 @@ +package touch.baton.domain.notification.query.controller; + +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.RestController; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.notification.command.Notification; +import touch.baton.domain.notification.query.controller.response.NotificationResponses; +import touch.baton.domain.notification.query.service.NotificationQueryService; +import touch.baton.domain.oauth.query.controller.resolver.AuthMemberPrincipal; + +import java.util.List; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/notifications") +@RestController +public class NotificationQueryController { + + private static final int READ_NOTIFICATION_DEFAULT_LIMIT = 10; + + private final NotificationQueryService notificationQueryService; + + @GetMapping + public ResponseEntity readNotificationsByMember(@AuthMemberPrincipal final Member member) { + final List foundNotifications = notificationQueryService.readNotificationsByMemberId(member.getId(), READ_NOTIFICATION_DEFAULT_LIMIT); + + return ResponseEntity.ok(NotificationResponses.SimpleNotifications.from(foundNotifications)); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/notification/query/controller/response/NotificationResponse.java b/backend/baton/src/main/java/touch/baton/domain/notification/query/controller/response/NotificationResponse.java new file mode 100644 index 000000000..ebd6f789a --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/notification/query/controller/response/NotificationResponse.java @@ -0,0 +1,31 @@ +package touch.baton.domain.notification.query.controller.response; + +import touch.baton.domain.notification.command.Notification; +import touch.baton.domain.notification.command.vo.NotificationType; + +import java.time.LocalDateTime; + +public record NotificationResponse() { + + public record Simple(Long notificationId, + String title, + String message, + NotificationType notificationType, + Long referencedId, + boolean isRead, + LocalDateTime createdAt + ) { + + public static Simple from(final Notification notification) { + return new NotificationResponse.Simple( + notification.getId(), + notification.getNotificationTitle().getValue(), + notification.getNotificationMessage().getValue(), + notification.getNotificationType(), + notification.getNotificationReferencedId().getValue(), + notification.getIsRead().getValue(), + notification.getCreatedAt() + ); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/notification/query/controller/response/NotificationResponses.java b/backend/baton/src/main/java/touch/baton/domain/notification/query/controller/response/NotificationResponses.java new file mode 100644 index 000000000..d830165ef --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/notification/query/controller/response/NotificationResponses.java @@ -0,0 +1,19 @@ +package touch.baton.domain.notification.query.controller.response; + +import touch.baton.domain.notification.command.Notification; + +import java.util.List; + +public record NotificationResponses() { + + public record SimpleNotifications(List data) { + + public static SimpleNotifications from(final List notifications) { + final List response = notifications.stream() + .map(NotificationResponse.Simple::from) + .toList(); + + return new SimpleNotifications(response); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/notification/query/repository/NotificationQuerydslRepository.java b/backend/baton/src/main/java/touch/baton/domain/notification/query/repository/NotificationQuerydslRepository.java new file mode 100644 index 000000000..d723ac8cf --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/notification/query/repository/NotificationQuerydslRepository.java @@ -0,0 +1,27 @@ +package touch.baton.domain.notification.query.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import touch.baton.domain.notification.command.Notification; + +import java.util.List; + +import static touch.baton.domain.notification.command.QNotification.notification; + +@RequiredArgsConstructor +@Repository +public class NotificationQuerydslRepository { + + private final JPAQueryFactory jpaQueryFactory; + + public List findByMemberId(final Long memberId, + final int limit + ) { + return jpaQueryFactory.selectFrom(notification) + .where(notification.member.id.eq(memberId)) + .orderBy(notification.id.desc()) + .limit(limit) + .fetch(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/notification/query/service/NotificationQueryService.java b/backend/baton/src/main/java/touch/baton/domain/notification/query/service/NotificationQueryService.java new file mode 100644 index 000000000..ece728ab8 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/notification/query/service/NotificationQueryService.java @@ -0,0 +1,21 @@ +package touch.baton.domain.notification.query.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import touch.baton.domain.notification.command.Notification; +import touch.baton.domain.notification.query.repository.NotificationQuerydslRepository; + +import java.util.List; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class NotificationQueryService { + + private final NotificationQuerydslRepository notificationQuerydslRepository; + + public List readNotificationsByMemberId(final Long memberId, final int limit) { + return notificationQuerydslRepository.findByMemberId(memberId, limit); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/command/AuthorizationHeader.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/AuthorizationHeader.java new file mode 100644 index 000000000..24083a6c3 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/AuthorizationHeader.java @@ -0,0 +1,27 @@ +package touch.baton.domain.oauth.command; + +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/command/OauthInformation.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/OauthInformation.java new file mode 100644 index 000000000..6f53e8740 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/OauthInformation.java @@ -0,0 +1,48 @@ +package touch.baton.domain.oauth.command; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import touch.baton.domain.member.command.vo.GithubUrl; +import touch.baton.domain.member.command.vo.ImageUrl; +import touch.baton.domain.member.command.vo.MemberName; +import touch.baton.domain.member.command.vo.OauthId; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.oauth.command.token.SocialToken; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +public class OauthInformation { + + private SocialToken socialToken; + + private OauthId oauthId; + + private MemberName memberName; + + private SocialId socialId; + + private GithubUrl githubUrl; + + private ImageUrl imageUrl; + + @Builder + private OauthInformation(final SocialToken socialToken, + final OauthId oauthId, + final MemberName memberName, + final SocialId socialId, + final GithubUrl githubUrl, + final ImageUrl imageUrl + ) { + this.socialToken = socialToken; + this.oauthId = oauthId; + this.memberName = memberName; + this.socialId = socialId; + this.githubUrl = githubUrl; + this.imageUrl = imageUrl; + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/command/OauthType.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/OauthType.java new file mode 100644 index 000000000..7c7a46a5b --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/OauthType.java @@ -0,0 +1,12 @@ +package touch.baton.domain.oauth.command; + +import static java.util.Locale.ENGLISH; + +public enum OauthType { + + GITHUB; + + public static OauthType from(final String name) { + return OauthType.valueOf(name.toUpperCase(ENGLISH)); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/command/authcode/AuthCodeRequestUrlProvider.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/authcode/AuthCodeRequestUrlProvider.java new file mode 100644 index 000000000..b67c059b6 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/authcode/AuthCodeRequestUrlProvider.java @@ -0,0 +1,10 @@ +package touch.baton.domain.oauth.command.authcode; + +import touch.baton.domain.oauth.command.OauthType; + +public interface AuthCodeRequestUrlProvider { + + OauthType oauthServer(); + + String getRequestUrl(); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/command/authcode/AuthCodeRequestUrlProviderComposite.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/authcode/AuthCodeRequestUrlProviderComposite.java new file mode 100644 index 000000000..a50630956 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/authcode/AuthCodeRequestUrlProviderComposite.java @@ -0,0 +1,37 @@ +package touch.baton.domain.oauth.command.authcode; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.oauth.command.OauthType; +import touch.baton.domain.oauth.command.exception.OauthRequestException; + +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.util.function.Function.identity; + +@Profile("!test") +@Component +public class AuthCodeRequestUrlProviderComposite { + + private final Map authCodeProviders; + + public AuthCodeRequestUrlProviderComposite(final Set authCodeProviders) { + this.authCodeProviders = authCodeProviders.stream() + .collect(Collectors.toMap( + AuthCodeRequestUrlProvider::oauthServer, identity() + )); + } + + public String findRequestUrl(final OauthType oauthType) { + return findAuthCodeProvider(oauthType).getRequestUrl(); + } + + private AuthCodeRequestUrlProvider findAuthCodeProvider(final OauthType oauthType) { + return Optional.ofNullable(authCodeProviders.get(oauthType)) + .orElseThrow(() -> new OauthRequestException(ClientErrorCode.OAUTH_REQUEST_URL_PROVIDER_IS_WRONG)); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/command/client/OauthInformationClient.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/client/OauthInformationClient.java new file mode 100644 index 000000000..211358276 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/client/OauthInformationClient.java @@ -0,0 +1,11 @@ +package touch.baton.domain.oauth.command.client; + +import touch.baton.domain.oauth.command.OauthInformation; +import touch.baton.domain.oauth.command.OauthType; + +public interface OauthInformationClient { + + OauthType oauthType(); + + OauthInformation fetchInformation(final String authCode); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/command/client/OauthInformationClientComposite.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/client/OauthInformationClientComposite.java new file mode 100644 index 000000000..25d47e232 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/client/OauthInformationClientComposite.java @@ -0,0 +1,36 @@ +package touch.baton.domain.oauth.command.client; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.oauth.command.OauthInformation; +import touch.baton.domain.oauth.command.OauthType; +import touch.baton.domain.oauth.command.exception.OauthRequestException; + +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.util.function.Function.identity; + +@Profile("!test") +@Component +public class OauthInformationClientComposite { + + private final Map clients; + + public OauthInformationClientComposite(final Set clients) { + this.clients = clients.stream() + .collect(Collectors.toMap( + OauthInformationClient::oauthType, + identity() + )); + } + + public OauthInformation fetchInformation(final OauthType oauthType, final String authCode) { + return Optional.ofNullable(clients.get(oauthType)) + .orElseThrow(() -> new OauthRequestException(ClientErrorCode.OAUTH_INFORMATION_CLIENT_IS_WRONG)) + .fetchInformation(authCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/command/controller/OauthCommandController.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/controller/OauthCommandController.java new file mode 100644 index 000000000..497888c99 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/controller/OauthCommandController.java @@ -0,0 +1,106 @@ +package touch.baton.domain.oauth.command.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.PatchMapping; +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.member.command.Member; +import touch.baton.domain.oauth.command.AuthorizationHeader; +import touch.baton.domain.oauth.command.OauthType; +import touch.baton.domain.oauth.command.service.OauthCommandService; +import touch.baton.domain.oauth.command.token.RefreshToken; +import touch.baton.domain.oauth.command.token.Tokens; +import touch.baton.domain.oauth.query.controller.resolver.AuthMemberPrincipal; + +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; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/oauth") +@RestController +public class OauthCommandController { + + private final OauthCommandService oauthCommandService; + + @GetMapping("/{oauthType}") + public ResponseEntity redirectAuthCode(@PathVariable("oauthType") final OauthType oauthType, + final HttpServletResponse response + ) throws IOException { + final String redirectUrl = oauthCommandService.readAuthCodeRedirect(oauthType); + response.sendRedirect(redirectUrl); + + return ResponseEntity.status(FOUND).build(); + } + + @GetMapping("/login/{oauthType}") + public ResponseEntity login(@PathVariable final OauthType oauthType, + @RequestParam final String code, + final HttpServletResponse response + ) { + final Tokens tokens = oauthCommandService.login(oauthType, code); + + setCookie(response, tokens.refreshToken()); + + return ResponseEntity.ok() + .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 = oauthCommandService.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()); + } + + @PatchMapping("/logout") + public ResponseEntity logout(@AuthMemberPrincipal final Member member) { + oauthCommandService.logout(member); + + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/command/exception/OauthBusinessException.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/exception/OauthBusinessException.java new file mode 100644 index 000000000..386b2cd2d --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/exception/OauthBusinessException.java @@ -0,0 +1,10 @@ +package touch.baton.domain.oauth.command.exception; + +import touch.baton.domain.common.exception.BusinessException; + +public class OauthBusinessException extends BusinessException { + + public OauthBusinessException(final String message) { + super(message); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/command/exception/OauthRequestException.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/exception/OauthRequestException.java new file mode 100644 index 000000000..f7dd10b57 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/exception/OauthRequestException.java @@ -0,0 +1,11 @@ +package touch.baton.domain.oauth.command.exception; + +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.ClientRequestException; + +public class OauthRequestException extends ClientRequestException { + + public OauthRequestException(final ClientErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/command/repository/OauthMemberCommandRepository.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/repository/OauthMemberCommandRepository.java new file mode 100644 index 000000000..7a1cbc420 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/repository/OauthMemberCommandRepository.java @@ -0,0 +1,15 @@ +package touch.baton.domain.oauth.command.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.vo.OauthId; +import touch.baton.domain.member.command.vo.SocialId; + +import java.util.Optional; + +public interface OauthMemberCommandRepository extends JpaRepository { + + Optional findMemberByOauthId(final OauthId oauthId); + + Optional findBySocialId(final SocialId socialId); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/command/repository/OauthRunnerCommandRepository.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/repository/OauthRunnerCommandRepository.java new file mode 100644 index 000000000..c25d25b64 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/repository/OauthRunnerCommandRepository.java @@ -0,0 +1,20 @@ +package touch.baton.domain.oauth.command.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.member.command.Runner; +import touch.baton.domain.member.command.vo.SocialId; + +import java.util.Optional; + +public interface OauthRunnerCommandRepository extends JpaRepository { + + @Query(""" + select r, r.member + from Runner r + join fetch Member m on m.id = r.member.id + where m.socialId = :socialId + """) + Optional joinByMemberSocialId(@Param("socialId") final SocialId socialId); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/command/repository/OauthSupporterCommandRepository.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/repository/OauthSupporterCommandRepository.java new file mode 100644 index 000000000..de00ec030 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/repository/OauthSupporterCommandRepository.java @@ -0,0 +1,20 @@ +package touch.baton.domain.oauth.command.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.member.command.Supporter; +import touch.baton.domain.member.command.vo.SocialId; + +import java.util.Optional; + +public interface OauthSupporterCommandRepository extends JpaRepository { + + @Query(""" + select s, s.member + from Supporter s + join fetch Member m on m.id = s.member.id + where m.socialId = :socialId + """) + Optional joinByMemberSocialId(@Param("socialId") final SocialId socialId); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/command/repository/RefreshTokenCommandRepository.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/repository/RefreshTokenCommandRepository.java new file mode 100644 index 000000000..c973ffde4 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/repository/RefreshTokenCommandRepository.java @@ -0,0 +1,17 @@ +package touch.baton.domain.oauth.command.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.oauth.command.token.RefreshToken; +import touch.baton.domain.oauth.command.token.Token; + +import java.util.Optional; + +public interface RefreshTokenCommandRepository extends JpaRepository { + + Optional findByToken(final Token token); + + Optional findByMember(final Member member); + + void deleteByMember(final Member member); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/command/service/OauthCommandService.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/service/OauthCommandService.java new file mode 100644 index 000000000..078649e85 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/service/OauthCommandService.java @@ -0,0 +1,166 @@ +package touch.baton.domain.oauth.command.service; + +import io.jsonwebtoken.Claims; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.vo.Company; +import touch.baton.domain.member.command.vo.ReviewCount; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.oauth.command.AuthorizationHeader; +import touch.baton.domain.oauth.command.OauthInformation; +import touch.baton.domain.oauth.command.OauthType; +import touch.baton.domain.oauth.command.authcode.AuthCodeRequestUrlProviderComposite; +import touch.baton.domain.oauth.command.client.OauthInformationClientComposite; +import touch.baton.domain.oauth.command.exception.OauthRequestException; +import touch.baton.domain.oauth.command.repository.OauthMemberCommandRepository; +import touch.baton.domain.oauth.command.repository.OauthRunnerCommandRepository; +import touch.baton.domain.oauth.command.repository.OauthSupporterCommandRepository; +import touch.baton.domain.oauth.command.repository.RefreshTokenCommandRepository; +import touch.baton.domain.oauth.command.token.AccessToken; +import touch.baton.domain.oauth.command.token.ExpireDate; +import touch.baton.domain.oauth.command.token.RefreshToken; +import touch.baton.domain.oauth.command.token.Token; +import touch.baton.domain.oauth.command.token.Tokens; +import touch.baton.domain.technicaltag.command.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 +@Service +public class OauthCommandService { + + private final AuthCodeRequestUrlProviderComposite authCodeRequestUrlProviderComposite; + private final OauthInformationClientComposite oauthInformationClientComposite; + private final OauthMemberCommandRepository oauthMemberCommandRepository; + private final OauthRunnerCommandRepository oauthRunnerCommandRepository; + private final OauthSupporterCommandRepository oauthSupporterCommandRepository; + private final RefreshTokenCommandRepository refreshTokenCommandRepository; + 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 Tokens login(final OauthType oauthType, final String code) { + final OauthInformation oauthInformation = oauthInformationClientComposite.fetchInformation(oauthType, code); + + final Optional maybeMember = oauthMemberCommandRepository.findMemberByOauthId(oauthInformation.getOauthId()); + if (maybeMember.isEmpty()) { + final Member savedMember = signUpMember(oauthInformation); + saveNewRunner(savedMember); + saveNewSupporter(savedMember); + return createTokens(oauthInformation.getSocialId(), savedMember); + } + + return createTokens(oauthInformation.getSocialId(), maybeMember.get()); + } + + private Member signUpMember(final OauthInformation oauthInformation) { + final Member newMember = Member.builder() + .memberName(oauthInformation.getMemberName()) + .socialId(oauthInformation.getSocialId()) + .oauthId(oauthInformation.getOauthId()) + .githubUrl(oauthInformation.getGithubUrl()) + .company(new Company("")) + .imageUrl(oauthInformation.getImageUrl()) + .build(); + + return oauthMemberCommandRepository.save(newMember); + } + + private Runner saveNewRunner(final Member member) { + final Runner newRunner = Runner.builder() + .member(member) + .build(); + + return oauthRunnerCommandRepository.save(newRunner); + } + + private Supporter saveNewSupporter(final Member member) { + final Supporter newSupporter = Supporter.builder() + .reviewCount(new ReviewCount(0)) + .member(member) + .supporterTechnicalTags(new SupporterTechnicalTags(new ArrayList<>())) + .build(); + + return oauthSupporterCommandRepository.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 = refreshTokenCommandRepository.findByMember(member); + if (maybeRefreshToken.isPresent()) { + final RefreshToken findRefreshToken = maybeRefreshToken.get(); + findRefreshToken.updateToken(new Token(randomTokens), refreshTokenExpireMinutes); + return new Tokens(accessToken, findRefreshToken); + } + + refreshTokenCommandRepository.save(refreshToken); + return new Tokens(accessToken, refreshToken); + } + + 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 = oauthMemberCommandRepository.findBySocialId(socialId) + .orElseThrow(() -> new OauthRequestException(ClientErrorCode.JWT_CLAIM_SOCIAL_ID_IS_WRONG)); + + final RefreshToken findRefreshToken = refreshTokenCommandRepository.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); + } + + public void logout(final Member member) { + refreshTokenCommandRepository.deleteByMember(member); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/command/token/AccessToken.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/token/AccessToken.java new file mode 100644 index 000000000..9a377b429 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/token/AccessToken.java @@ -0,0 +1,15 @@ +package touch.baton.domain.oauth.command.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/command/token/ExpireDate.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/token/ExpireDate.java new file mode 100644 index 000000000..1efe615af --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/token/ExpireDate.java @@ -0,0 +1,40 @@ +package touch.baton.domain.oauth.command.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/command/token/RefreshToken.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/token/RefreshToken.java new file mode 100644 index 000000000..ca5a9e96f --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/token/RefreshToken.java @@ -0,0 +1,77 @@ +package touch.baton.domain.oauth.command.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.command.Member; +import touch.baton.domain.oauth.command.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/command/token/SocialToken.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/token/SocialToken.java new file mode 100644 index 000000000..2bd9e237b --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/token/SocialToken.java @@ -0,0 +1,15 @@ +package touch.baton.domain.oauth.command.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/command/token/Token.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/token/Token.java new file mode 100644 index 000000000..bbd692a5b --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/token/Token.java @@ -0,0 +1,31 @@ +package touch.baton.domain.oauth.command.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/command/token/Tokens.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/token/Tokens.java new file mode 100644 index 000000000..d66ae0d85 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/token/Tokens.java @@ -0,0 +1,7 @@ +package touch.baton.domain.oauth.command.token; + +public record Tokens( + AccessToken accessToken, + RefreshToken refreshToken +) { +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/command/token/exception/RefreshTokenDomainException.java b/backend/baton/src/main/java/touch/baton/domain/oauth/command/token/exception/RefreshTokenDomainException.java new file mode 100644 index 000000000..ab106c01d --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/command/token/exception/RefreshTokenDomainException.java @@ -0,0 +1,10 @@ +package touch.baton.domain.oauth.command.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/oauth/query/controller/resolver/AuthMemberPrincipal.java b/backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthMemberPrincipal.java new file mode 100644 index 000000000..9db9b8d0f --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthMemberPrincipal.java @@ -0,0 +1,13 @@ +package touch.baton.domain.oauth.query.controller.resolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuthMemberPrincipal { + + boolean required() default true; +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthMemberPrincipalArgumentResolver.java b/backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthMemberPrincipalArgumentResolver.java new file mode 100644 index 000000000..7cd9bb7e4 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthMemberPrincipalArgumentResolver.java @@ -0,0 +1,54 @@ +package touch.baton.domain.oauth.query.controller.resolver; + +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.vo.Company; +import touch.baton.domain.member.command.vo.GithubUrl; +import touch.baton.domain.member.command.vo.ImageUrl; +import touch.baton.domain.member.command.vo.MemberName; +import touch.baton.domain.member.command.vo.OauthId; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.oauth.command.exception.OauthRequestException; +import touch.baton.domain.oauth.command.repository.OauthMemberCommandRepository; +import touch.baton.infra.auth.jwt.JwtDecoder; + +@Component +public class AuthMemberPrincipalArgumentResolver extends UserPrincipalArgumentResolver { + + private final OauthMemberCommandRepository oauthMemberCommandRepository; + + public AuthMemberPrincipalArgumentResolver(final JwtDecoder jwtDecoder, final OauthMemberCommandRepository oauthMemberCommandRepository) { + super(jwtDecoder); + this.oauthMemberCommandRepository = oauthMemberCommandRepository; + } + + @Override + public boolean supportsParameter(final MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthMemberPrincipal.class); + } + + @Override + protected Object getGuest() { + return Member.builder() + .memberName(new MemberName("게스트")) + .socialId(new SocialId("게스트 SocialId")) + .oauthId(new OauthId("게스트 OauthId")) + .githubUrl(new GithubUrl("게스트 GitHubUrl")) + .company(new Company("게스트 회사")) + .imageUrl(new ImageUrl("게스트 이미지")) + .build(); + } + + @Override + protected Object getUser(final String socialId) { + return oauthMemberCommandRepository.findBySocialId(new SocialId(socialId)) + .orElseThrow(() -> new OauthRequestException(ClientErrorCode.JWT_CLAIM_SOCIAL_ID_IS_WRONG)); + } + + @Override + protected boolean isAuthorizationRequired(final MethodParameter parameter) { + return parameter.getParameterAnnotation(AuthMemberPrincipal.class).required(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthRunnerPrincipal.java b/backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthRunnerPrincipal.java new file mode 100644 index 000000000..1b5995033 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthRunnerPrincipal.java @@ -0,0 +1,13 @@ +package touch.baton.domain.oauth.query.controller.resolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuthRunnerPrincipal { + + boolean required() default true; +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthRunnerPrincipalArgumentResolver.java b/backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthRunnerPrincipalArgumentResolver.java new file mode 100644 index 000000000..71af5ec54 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthRunnerPrincipalArgumentResolver.java @@ -0,0 +1,57 @@ +package touch.baton.domain.oauth.query.controller.resolver; + +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.vo.Company; +import touch.baton.domain.member.command.vo.GithubUrl; +import touch.baton.domain.member.command.vo.ImageUrl; +import touch.baton.domain.member.command.vo.MemberName; +import touch.baton.domain.member.command.vo.OauthId; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.oauth.command.exception.OauthRequestException; +import touch.baton.domain.oauth.command.repository.OauthRunnerCommandRepository; +import touch.baton.infra.auth.jwt.JwtDecoder; + +@Component +public class AuthRunnerPrincipalArgumentResolver extends UserPrincipalArgumentResolver { + + private final OauthRunnerCommandRepository oauthRunnerCommandRepository; + + public AuthRunnerPrincipalArgumentResolver(final JwtDecoder jwtDecoder, final OauthRunnerCommandRepository oauthRunnerCommandRepository) { + super(jwtDecoder); + this.oauthRunnerCommandRepository = oauthRunnerCommandRepository; + } + + @Override + public boolean supportsParameter(final MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthRunnerPrincipal.class); + } + + @Override + protected Object getGuest() { + return Runner.builder() + .member(Member.builder() + .memberName(new MemberName("게스트")) + .socialId(new SocialId("guestSocialId")) + .oauthId(new OauthId("guestOauthId")) + .githubUrl(new GithubUrl("guestGithubUrl")) + .company(new Company("guestCompany")) + .imageUrl(new ImageUrl("guestImageUrl")) + .build() + ); + } + + @Override + protected Object getUser(final String socialId) { + return oauthRunnerCommandRepository.joinByMemberSocialId(new SocialId(socialId)) + .orElseThrow(() -> new OauthRequestException(ClientErrorCode.JWT_CLAIM_SOCIAL_ID_IS_WRONG)); + } + + @Override + protected boolean isAuthorizationRequired(final MethodParameter parameter) { + return parameter.getParameterAnnotation(AuthRunnerPrincipal.class).required(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthSupporterPrincipal.java b/backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthSupporterPrincipal.java new file mode 100644 index 000000000..95db43948 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthSupporterPrincipal.java @@ -0,0 +1,13 @@ +package touch.baton.domain.oauth.query.controller.resolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuthSupporterPrincipal { + + boolean required() default true; +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthSupporterPrincipalArgumentResolver.java b/backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthSupporterPrincipalArgumentResolver.java new file mode 100644 index 000000000..351ce9fb7 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/AuthSupporterPrincipalArgumentResolver.java @@ -0,0 +1,50 @@ +package touch.baton.domain.oauth.query.controller.resolver; + +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.vo.ReviewCount; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.oauth.command.exception.OauthRequestException; +import touch.baton.domain.oauth.command.repository.OauthSupporterCommandRepository; +import touch.baton.domain.technicaltag.command.SupporterTechnicalTags; +import touch.baton.infra.auth.jwt.JwtDecoder; + +import java.util.Collections; + +@Component +public class AuthSupporterPrincipalArgumentResolver extends UserPrincipalArgumentResolver { + + private final OauthSupporterCommandRepository oauthSupporterCommandRepository; + + public AuthSupporterPrincipalArgumentResolver(final JwtDecoder jwtDecoder, final OauthSupporterCommandRepository oauthSupporterCommandRepository) { + super(jwtDecoder); + this.oauthSupporterCommandRepository = oauthSupporterCommandRepository; + } + + @Override + public boolean supportsParameter(final MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthSupporterPrincipal.class); + } + + @Override + protected Object getGuest() { + return Supporter.builder() + .reviewCount(new ReviewCount(0)) + .member(null) + .supporterTechnicalTags(new SupporterTechnicalTags(Collections.emptyList())) + .build(); + } + + @Override + protected Object getUser(final String socialId) { + return oauthSupporterCommandRepository.joinByMemberSocialId(new SocialId(socialId)) + .orElseThrow(() -> new OauthRequestException(ClientErrorCode.JWT_CLAIM_SOCIAL_ID_IS_WRONG)); + } + + @Override + protected boolean isAuthorizationRequired(final MethodParameter parameter) { + return parameter.getParameterAnnotation(AuthSupporterPrincipal.class).required(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/UserPrincipalArgumentResolver.java b/backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/UserPrincipalArgumentResolver.java new file mode 100644 index 000000000..0b9ff74d3 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/oauth/query/controller/resolver/UserPrincipalArgumentResolver.java @@ -0,0 +1,61 @@ +package touch.baton.domain.oauth.query.controller.resolver; + +import io.jsonwebtoken.Claims; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +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.command.AuthorizationHeader; +import touch.baton.domain.oauth.command.exception.OauthRequestException; +import touch.baton.infra.auth.jwt.JwtDecoder; + +import java.util.Objects; + +import static org.springframework.http.HttpHeaders.AUTHORIZATION; + +@RequiredArgsConstructor +public abstract class UserPrincipalArgumentResolver implements HandlerMethodArgumentResolver { + + private static final String BEARER = "Bearer "; + + private final JwtDecoder jwtDecoder; + + @Override + public Object resolveArgument(final MethodParameter parameter, + final ModelAndViewContainer mavContainer, + final NativeWebRequest webRequest, + final WebDataBinderFactory binderFactory + ) { + final boolean isAuthorizationRequired = isAuthorizationRequired(parameter); + final boolean isAuthorizationHeaderNotExist = isAuthorizationHeaderNotExist(webRequest); + if (isAuthorizationRequired && isAuthorizationHeaderNotExist) { + throw new OauthRequestException(ClientErrorCode.OAUTH_AUTHORIZATION_VALUE_IS_NULL); + } + if (!isAuthorizationRequired && isAuthorizationHeaderNotExist) { + return getGuest(); + } + + final AuthorizationHeader authorization = new AuthorizationHeader(webRequest.getHeader(AUTHORIZATION)); + if (authorization.isNotBearerAuth()) { + throw new OauthRequestException(ClientErrorCode.OAUTH_AUTHORIZATION_BEARER_TYPE_NOT_FOUND); + } + + final Claims claims = jwtDecoder.parseAuthorizationHeader(authorization); + final String socialId = claims.get("socialId", String.class); + + return getUser(socialId); + } + + private boolean isAuthorizationHeaderNotExist(final NativeWebRequest webRequest) { + return Objects.isNull(webRequest.getHeader(AUTHORIZATION)); + } + + protected abstract boolean isAuthorizationRequired(final MethodParameter parameter); + + protected abstract Object getGuest(); + + protected abstract Object getUser(final String socialId); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/RunnerPost.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/RunnerPost.java new file mode 100644 index 000000000..bc5b983bc --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/RunnerPost.java @@ -0,0 +1,299 @@ +package touch.baton.domain.runnerpost.command; + +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import touch.baton.domain.common.BaseEntity; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.runnerpost.command.exception.RunnerPostDomainException; +import touch.baton.domain.runnerpost.command.vo.CuriousContents; +import touch.baton.domain.runnerpost.command.vo.Deadline; +import touch.baton.domain.runnerpost.command.vo.ImplementedContents; +import touch.baton.domain.runnerpost.command.vo.IsReviewed; +import touch.baton.domain.runnerpost.command.vo.PostscriptContents; +import touch.baton.domain.runnerpost.command.vo.PullRequestUrl; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.command.vo.Title; +import touch.baton.domain.runnerpost.command.vo.WatchedCount; +import touch.baton.domain.tag.command.RunnerPostTag; +import touch.baton.domain.tag.command.RunnerPostTags; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static jakarta.persistence.EnumType.STRING; +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class RunnerPost extends BaseEntity { + + @GeneratedValue(strategy = IDENTITY) + @Id + private Long id; + + @Embedded + private Title title; + + @Embedded + private ImplementedContents implementedContents; + + @Embedded + private CuriousContents curiousContents; + + @Embedded + private PostscriptContents postscriptContents; + + @Embedded + private PullRequestUrl pullRequestUrl; + + @Embedded + private Deadline deadline; + + @Embedded + private WatchedCount watchedCount; + + @Enumerated(STRING) + @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; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "supporter_id", foreignKey = @ForeignKey(name = "fk_runner_post_to_supporter"), nullable = true) + private Supporter supporter; + + @Embedded + private RunnerPostTags runnerPostTags; + + @Builder + private RunnerPost(final Title title, + 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, implementedContents, curiousContents, postscriptContents, pullRequestUrl, deadline, watchedCount, reviewStatus, isReviewed, runner, supporter, runnerPostTags); + } + + private RunnerPost(final Long id, + final Title title, + 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, implementedContents, curiousContents, postscriptContents, pullRequestUrl, deadline, watchedCount, reviewStatus, isReviewed, runner, runnerPostTags); + this.id = id; + this.title = title; + 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 implementedContents, + final String curiousContents, + final String postscriptContents, + final String pullRequestUrl, + final LocalDateTime deadline, + final Runner runner + ) { + return RunnerPost.builder() + .title(new Title(title)) + .implementedContents(new ImplementedContents(implementedContents)) + .curiousContents(new CuriousContents(curiousContents)) + .postscriptContents(new PostscriptContents(postscriptContents)) + .pullRequestUrl(new PullRequestUrl(pullRequestUrl)) + .deadline(new Deadline(deadline)) + .watchedCount(WatchedCount.zero()) + .reviewStatus(ReviewStatus.NOT_STARTED) + .isReviewed(IsReviewed.notReviewed()) + .runner(runner) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .build(); + } + + private void validateNotNull(final Title title, + 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(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 일 수 없습니다."); + } + } + + public void addAllRunnerPostTags(final List postTags) { + runnerPostTags.addAll(postTags); + } + + public void finishReview() { + updateReviewStatus(ReviewStatus.DONE); + } + + public void finishFeedback() { + this.isReviewed = IsReviewed.reviewed(); + } + + public void updateReviewStatus(final ReviewStatus other) { + if (this.reviewStatus.isSame(ReviewStatus.NOT_STARTED) && other.isSame(ReviewStatus.IN_PROGRESS)) { + throw new RunnerPostDomainException("ReviewStatus 를 수정하던 도중 NOT_STARTED 에서 IN_PROGRESS 로 리뷰 상태 정책을 원인으로 실패하였습니다."); + } + if (this.reviewStatus.isSame(ReviewStatus.NOT_STARTED) && other.isSame(ReviewStatus.DONE)) { + throw new RunnerPostDomainException("ReviewStatus 를 수정하던 도중 NOT_STARTED 에서 DONE 으로 리뷰 상태 정책을 원인으로 실패하였습니다."); + } + if (this.reviewStatus.isSame(ReviewStatus.DONE) && other.isSame(ReviewStatus.NOT_STARTED)) { + throw new RunnerPostDomainException("ReviewStatus 를 수정하던 도중 DONE 에서 NOT_STARTED 로 리뷰 상태 정책을 원인으로 실패하였습니다."); + } + if (this.reviewStatus.isSame(ReviewStatus.DONE) && other.isSame(ReviewStatus.IN_PROGRESS)) { + throw new RunnerPostDomainException("ReviewStatus 를 수정하던 도중 DONE 에서 IN_PROGRESS 로 리뷰 상태 정책을 원인으로 실패하였습니다."); + } + if (this.reviewStatus.isSame(ReviewStatus.DONE) && other.isSame(ReviewStatus.OVERDUE)) { + throw new RunnerPostDomainException("ReviewStatus 를 수정하던 도중 DONE 에서 OVERDUE 로 리뷰 상태 정책을 원인으로 실패하였습니다."); + } + if (this.reviewStatus.isSame(ReviewStatus.OVERDUE) && other.isSame(ReviewStatus.DONE)) { + throw new RunnerPostDomainException("ReviewStatus 를 수정하던 도중 OVERDUE 에서 DONE 로 리뷰 상태 정책을 원인으로 실패하였습니다."); + } + if (this.reviewStatus.isSame(other)) { + throw new RunnerPostDomainException("ReviewStatus 를 수정하던 도중 같은 ReviewStatus 로 리뷰 상태 정책을 원인으로 실패하였습니다."); + } + + this.reviewStatus = other; + } + + public void assignSupporter(final Supporter supporter) { + if (Objects.nonNull(this.supporter)) { + throw new RunnerPostDomainException("Supporter 를 할당하던 도중 RunnerPost 에 이미 다른 Supporter 가 할당되어 있는 것을 원인으로 실패하였습니다."); + } + if (reviewStatus.isSame(ReviewStatus.OVERDUE)) { + throw new RunnerPostDomainException("Supporter 를 할당하던 도중 ReviewStatus 가 OVERDUE 상태가 원인으로 실패하였습니다."); + } + if (reviewStatus.isNotSameAsNotStarted()) { + throw new RunnerPostDomainException("Supporter 를 할당하던 도중 ReviewStatus 가 NOT_STARTED 상태가 아닌 것을 원인으로 실패하였습니다."); + } + if (deadline.isEnd()) { + throw new RunnerPostDomainException("Supporter 를 할당하던 도중 ReviewStatus 의 Deadline 이 현재 시간보다 과거인 것을 원인으로 실패하였습니다."); + } + + this.supporter = supporter; + this.reviewStatus = ReviewStatus.IN_PROGRESS; + } + + public void increaseWatchedCount() { + this.watchedCount = watchedCount.increase(); + } + + public boolean isNotOwner(final Runner targetRunner) { + return !runner.equals(targetRunner); + } + + public boolean isOwner(final Member targetMember) { + return runner.getMember().equals(targetMember); + } + + public boolean isReviewStatusStarted() { + return !(reviewStatus.isNotStarted() || reviewStatus.isOverdue()); + } + + public boolean isDifferentSupporter(final Supporter targetSupporter) { + return !supporter.equals(targetSupporter); + } + + public boolean isReviewStatusNotStarted() { + return reviewStatus.isNotStarted(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final RunnerPost that = (RunnerPost) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/RunnerPostsApplicantCount.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/RunnerPostsApplicantCount.java new file mode 100644 index 000000000..50c80b3f2 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/RunnerPostsApplicantCount.java @@ -0,0 +1,46 @@ +package touch.baton.domain.runnerpost.command; + +import touch.baton.domain.runnerpost.command.exception.RunnerPostBusinessException; +import touch.baton.domain.runnerpost.command.repository.dto.RunnerPostApplicantCountDto; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class RunnerPostsApplicantCount { + + private final Map runnerPostsApplicantCount; + + private RunnerPostsApplicantCount(final Map runnerPostsApplicantCount) { + this.runnerPostsApplicantCount = runnerPostsApplicantCount; + } + + public static RunnerPostsApplicantCount from(final List dtos) { + validateDtosNotNull(dtos); + final Map runnerPostApplicantCounts = dtos.stream() + .collect(Collectors.toMap( + RunnerPostApplicantCountDto::runnerPostId, + RunnerPostApplicantCountDto::applicantCount + )); + return new RunnerPostsApplicantCount(runnerPostApplicantCounts); + } + + private static void validateDtosNotNull(final List dtos) { + if (Objects.isNull(dtos)) { + throw new RunnerPostBusinessException("RunnerPostsApplicantCount 를 생성할 때 생성자인 dto 가 null 입니다."); + } + } + + public Long getApplicantCountById(final Long runnerPostId) { + final Long readApplicantCount = runnerPostsApplicantCount.get(runnerPostId); + validateElementExist(readApplicantCount); + return readApplicantCount; + } + + private void validateElementExist(final Long readElement) { + if (Objects.isNull(readElement)) { + throw new RunnerPostBusinessException(String.format("%s 에 없는 RunnerPostId 입니다.", this.getClass().getSimpleName())); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/controller/RunnerPostCommandController.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/controller/RunnerPostCommandController.java new file mode 100644 index 000000000..664570311 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/controller/RunnerPostCommandController.java @@ -0,0 +1,106 @@ +package touch.baton.domain.runnerpost.command.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +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 org.springframework.web.util.UriComponentsBuilder; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.oauth.query.controller.resolver.AuthRunnerPrincipal; +import touch.baton.domain.oauth.query.controller.resolver.AuthSupporterPrincipal; +import touch.baton.domain.runnerpost.command.service.RunnerPostCommandService; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostApplicantCreateRequest; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostCreateRequest; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostUpdateRequest; + +import java.net.URI; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/posts/runner") +@RestController +public class RunnerPostCommandController { + + private final RunnerPostCommandService runnerPostCommandService; + + @PostMapping + public ResponseEntity createRunnerPost(@AuthRunnerPrincipal final Runner runner, + @Valid @RequestBody final RunnerPostCreateRequest request + ) { + final Long savedId = runnerPostCommandService.createRunnerPost(runner, request); + final URI redirectUri = UriComponentsBuilder.fromPath("/api/v1/posts/runner") + .path("/{id}") + .buildAndExpand(savedId) + .toUri(); + return ResponseEntity.created(redirectUri).build(); + } + + @DeleteMapping("/{runnerPostId}") + public ResponseEntity deleteByRunnerPostId(@AuthRunnerPrincipal final Runner runner, + @PathVariable final Long runnerPostId + ) { + runnerPostCommandService.deleteByRunnerPostId(runnerPostId, runner); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/{runnerPostId}/application") + public ResponseEntity createRunnerPostApplicant( + @AuthSupporterPrincipal final Supporter supporter, + @PathVariable final Long runnerPostId, + @RequestBody @Valid final RunnerPostApplicantCreateRequest request + ) { + runnerPostCommandService.createRunnerPostApplicant(supporter, request, runnerPostId); + + final URI redirectUri = UriComponentsBuilder.fromPath("/api/v1/posts/runner") + .path("/{runnerPostId}") + .buildAndExpand(runnerPostId) + .toUri(); + + return ResponseEntity.created(redirectUri).build(); + } + + @PatchMapping("/{runnerPostId}/cancelation") + public ResponseEntity updateSupporterCancelRunnerPost(@AuthSupporterPrincipal final Supporter supporter, + @PathVariable final Long runnerPostId + ) { + runnerPostCommandService.deleteSupporterRunnerPost(supporter, runnerPostId); + final URI redirectUri = UriComponentsBuilder.fromPath("/api/v1/posts/runner") + .path("/{runnerPostId}") + .buildAndExpand(runnerPostId) + .toUri(); + return ResponseEntity.noContent().location(redirectUri).build(); + } + + @PatchMapping("/{runnerPostId}/supporters") + public ResponseEntity updateRunnerPostAppliedSupporter(@AuthRunnerPrincipal final Runner runner, + @PathVariable final Long runnerPostId, + @Valid @RequestBody final RunnerPostUpdateRequest.SelectSupporter request + ) { + runnerPostCommandService.updateRunnerPostAppliedSupporter(runner, runnerPostId, request); + + final URI redirectUri = UriComponentsBuilder.fromPath("/api/v1/posts/runner") + .path("/{runnerPostId}") + .buildAndExpand(runnerPostId) + .toUri(); + return ResponseEntity.noContent().location(redirectUri).build(); + } + + @PatchMapping("/{runnerPostId}/done") + public ResponseEntity updateRunnerPostReviewStatusDone(@AuthSupporterPrincipal final Supporter supporter, + @PathVariable final Long runnerPostId + ) { + runnerPostCommandService.updateRunnerPostReviewStatusDone(runnerPostId, supporter); + + final URI redirectUri = UriComponentsBuilder.fromPath("/api/v1/posts/runner") + .path("/{runnerPostId}") + .buildAndExpand(runnerPostId) + .toUri(); + return ResponseEntity.noContent().location(redirectUri).build(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/event/RunnerPostApplySupporterEvent.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/event/RunnerPostApplySupporterEvent.java new file mode 100644 index 000000000..2262029fd --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/event/RunnerPostApplySupporterEvent.java @@ -0,0 +1,4 @@ +package touch.baton.domain.runnerpost.command.event; + +public record RunnerPostApplySupporterEvent(Long runnerPostId) { +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/event/RunnerPostAssignSupporterEvent.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/event/RunnerPostAssignSupporterEvent.java new file mode 100644 index 000000000..0f443233c --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/event/RunnerPostAssignSupporterEvent.java @@ -0,0 +1,4 @@ +package touch.baton.domain.runnerpost.command.event; + +public record RunnerPostAssignSupporterEvent(Long runnerPostId) { +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/event/RunnerPostReviewStatusDoneEvent.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/event/RunnerPostReviewStatusDoneEvent.java new file mode 100644 index 000000000..60b84a5ef --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/event/RunnerPostReviewStatusDoneEvent.java @@ -0,0 +1,4 @@ +package touch.baton.domain.runnerpost.command.event; + +public record RunnerPostReviewStatusDoneEvent(Long runnerPostId) { +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/RunnerPostBusinessException.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/RunnerPostBusinessException.java new file mode 100644 index 000000000..7501b2abb --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/RunnerPostBusinessException.java @@ -0,0 +1,10 @@ +package touch.baton.domain.runnerpost.command.exception; + +import touch.baton.domain.common.exception.BusinessException; + +public class RunnerPostBusinessException extends BusinessException { + + public RunnerPostBusinessException(final String message) { + super(message); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/RunnerPostDomainException.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/RunnerPostDomainException.java new file mode 100644 index 000000000..abd7c03c5 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/RunnerPostDomainException.java @@ -0,0 +1,10 @@ +package touch.baton.domain.runnerpost.command.exception; + +import touch.baton.domain.common.exception.DomainException; + +public class RunnerPostDomainException extends DomainException { + + public RunnerPostDomainException(final String message) { + super(message); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/RunnerPostRequestException.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/RunnerPostRequestException.java new file mode 100644 index 000000000..015883b55 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/RunnerPostRequestException.java @@ -0,0 +1,11 @@ +package touch.baton.domain.runnerpost.command.exception; + +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.ClientRequestException; + +public class RunnerPostRequestException extends ClientRequestException { + + public RunnerPostRequestException(final ClientErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/FutureValidator.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/FutureValidator.java new file mode 100644 index 000000000..704e76184 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/FutureValidator.java @@ -0,0 +1,27 @@ +package touch.baton.domain.runnerpost.command.exception.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.ClientRequestException; + +import java.time.LocalDateTime; +import java.util.Objects; + +public class FutureValidator implements ConstraintValidator { + + private ClientErrorCode errorCode; + + @Override + public void initialize(final ValidFuture constraintAnnotation) { + errorCode = constraintAnnotation.clientErrorCode(); + } + + @Override + public boolean isValid(final LocalDateTime value, final ConstraintValidatorContext context) { + if (Objects.nonNull(value) && value.isBefore(LocalDateTime.now())) { + throw new ClientRequestException(errorCode); + } + return true; + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/MaxLengthValidator.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/MaxLengthValidator.java new file mode 100644 index 000000000..05d6bbc5f --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/MaxLengthValidator.java @@ -0,0 +1,28 @@ +package touch.baton.domain.runnerpost.command.exception.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.ClientRequestException; + +import java.util.Objects; + +public class MaxLengthValidator implements ConstraintValidator { + + private ClientErrorCode errorCode; + private int max; + + @Override + public void initialize(final ValidMaxLength constraintAnnotation) { + errorCode = constraintAnnotation.clientErrorCode(); + max = constraintAnnotation.max(); + } + + @Override + public boolean isValid(final String value, final ConstraintValidatorContext context) { + if (Objects.nonNull(value) && value.length() > max) { + throw new ClientRequestException(errorCode); + } + return true; + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/UrlValidator.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/UrlValidator.java new file mode 100644 index 000000000..486c151db --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/UrlValidator.java @@ -0,0 +1,34 @@ +package touch.baton.domain.runnerpost.command.exception.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.ClientRequestException; + +import java.util.Objects; +import java.util.regex.Pattern; + +public class UrlValidator implements ConstraintValidator { + + private static final Pattern urlPattern = Pattern.compile("https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#()?&//=]*)"); + + private ClientErrorCode errorCode; + + @Override + public void initialize(final ValidNotUrl constraintAnnotation) { + errorCode = constraintAnnotation.clientErrorCode(); + } + + @Override + public boolean isValid(final String value, final ConstraintValidatorContext context) { + if (Objects.nonNull(value) && isNotUrl(value)) { + throw new ClientRequestException(errorCode); + } + + return true; + } + + private boolean isNotUrl(final String value) { + return !urlPattern.matcher(value).matches(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/ValidFuture.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/ValidFuture.java new file mode 100644 index 000000000..fce07b991 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/ValidFuture.java @@ -0,0 +1,28 @@ +package touch.baton.domain.runnerpost.command.exception.validator; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import touch.baton.domain.common.exception.ClientErrorCode; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({FIELD, PARAMETER}) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = FutureValidator.class) +public @interface ValidFuture { + + String message() default "마감일은 오늘보다 과거일 수 없습니다."; + + Class[] groups() default {}; + + Class[] payload() default {}; + + ClientErrorCode clientErrorCode(); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/ValidMaxLength.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/ValidMaxLength.java new file mode 100644 index 000000000..242a3dc54 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/ValidMaxLength.java @@ -0,0 +1,30 @@ +package touch.baton.domain.runnerpost.command.exception.validator; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import touch.baton.domain.common.exception.ClientErrorCode; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({FIELD, PARAMETER}) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = MaxLengthValidator.class) +public @interface ValidMaxLength { + + String message() default "길이가 잘못되었습니다."; + + Class[] groups() default {}; + + Class[] payload() default {}; + + ClientErrorCode clientErrorCode(); + + int max(); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/ValidNotUrl.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/ValidNotUrl.java new file mode 100644 index 000000000..fe727797e --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/exception/validator/ValidNotUrl.java @@ -0,0 +1,28 @@ +package touch.baton.domain.runnerpost.command.exception.validator; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import touch.baton.domain.common.exception.ClientErrorCode; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({FIELD, PARAMETER}) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = UrlValidator.class) +public @interface ValidNotUrl { + + String message() default "PR 주소가 URL이 아닙니다."; + + Class[] groups() default {}; + + Class[] payload() default {}; + + ClientErrorCode clientErrorCode(); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/repository/RunnerPostCommandRepository.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/repository/RunnerPostCommandRepository.java new file mode 100644 index 000000000..99f9cae6e --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/repository/RunnerPostCommandRepository.java @@ -0,0 +1,7 @@ +package touch.baton.domain.runnerpost.command.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import touch.baton.domain.runnerpost.command.RunnerPost; + +public interface RunnerPostCommandRepository extends JpaRepository { +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/repository/dto/RunnerPostApplicantCountDto.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/repository/dto/RunnerPostApplicantCountDto.java new file mode 100644 index 000000000..1b9f66cc6 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/repository/dto/RunnerPostApplicantCountDto.java @@ -0,0 +1,4 @@ +package touch.baton.domain.runnerpost.command.repository.dto; + +public record RunnerPostApplicantCountDto(Long runnerPostId, long applicantCount) { +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/service/RunnerPostCommandService.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/service/RunnerPostCommandService.java new file mode 100644 index 000000000..6d6d2a292 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/service/RunnerPostCommandService.java @@ -0,0 +1,177 @@ +package touch.baton.domain.runnerpost.command.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.SupporterRunnerPost; +import touch.baton.domain.member.command.repository.SupporterCommandRepository; +import touch.baton.domain.member.command.repository.SupporterRunnerPostCommandRepository; +import touch.baton.domain.member.command.vo.Message; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.event.RunnerPostApplySupporterEvent; +import touch.baton.domain.runnerpost.command.event.RunnerPostAssignSupporterEvent; +import touch.baton.domain.runnerpost.command.event.RunnerPostReviewStatusDoneEvent; +import touch.baton.domain.runnerpost.command.exception.RunnerPostBusinessException; +import touch.baton.domain.runnerpost.command.repository.RunnerPostCommandRepository; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostApplicantCreateRequest; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostCreateRequest; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostUpdateRequest; +import touch.baton.domain.tag.command.RunnerPostTag; +import touch.baton.domain.tag.command.Tag; +import touch.baton.domain.tag.command.repository.TagCommandRepository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@RequiredArgsConstructor +@Transactional +@Service +public class RunnerPostCommandService { + + private final RunnerPostCommandRepository runnerPostCommandRepository; + private final TagCommandRepository tagCommandRepository; + private final SupporterCommandRepository supporterCommandRepository; + private final SupporterRunnerPostCommandRepository supporterRunnerPostCommandRepository; + private final ApplicationEventPublisher eventPublisher; + + public Long createRunnerPost(final Runner runner, final RunnerPostCreateRequest request) { + final RunnerPost runnerPost = toDomain(runner, request); + runnerPostCommandRepository.save(runnerPost); + + final List tags = findTagsAfterSave(request.tags()); + + final List runnerPostTags = tags.stream() + .map(tag -> RunnerPostTag.builder() + .tag(tag) + .runnerPost(runnerPost).build()) + .toList(); + + runnerPost.addAllRunnerPostTags(runnerPostTags); + return runnerPost.getId(); + } + + private RunnerPost toDomain(final Runner runner, final RunnerPostCreateRequest request) { + return RunnerPost.newInstance(request.title(), + request.implementedContents(), + request.curiousContents(), + request.postscriptContents(), + request.pullRequestUrl(), + request.deadline(), + runner); + } + + private List findTagsAfterSave(final List tagNames) { + final List tags = new ArrayList<>(); + for (String tagName : tagNames) { + tagCommandRepository.findByTagName(new TagName(tagName)) + .ifPresentOrElse(tags::add, addTagAfterSave(tags, tagName)); + } + + return tags; + } + + private Runnable addTagAfterSave(final List tags, final String tagName) { + return () -> { + final Tag savedTag = tagCommandRepository.save(Tag.newInstance(tagName)); + tags.add(savedTag); + }; + } + + public void deleteByRunnerPostId(final Long runnerPostId, final Runner runner) { + final RunnerPost runnerPost = runnerPostCommandRepository.findById(runnerPostId) + .orElseThrow(() -> new RunnerPostBusinessException("RunnerPost 의 식별자값으로 삭제할 러너 게시글이 존재하지 않습니다.")); + if (runnerPost.isNotOwner(runner)) { + throw new RunnerPostBusinessException("RunnerPost 를 게시한 유저가 아닙니다."); + } + if (runnerPost.isReviewStatusStarted()) { + throw new RunnerPostBusinessException("삭제할 수 없는 상태의 리뷰 상태입니다."); + } + if (supporterRunnerPostCommandRepository.existsByRunnerPostId(runnerPostId)) { + throw new RunnerPostBusinessException("지원자가 존재하여 삭제할 수 없습니다."); + } + runnerPostCommandRepository.deleteById(runnerPostId); + } + + private RunnerPost getRunnerPostOrThrowException(final Long runnerPostId) { + return runnerPostCommandRepository.findById(runnerPostId) + .orElseThrow(() -> new RunnerPostBusinessException("해당 runnerPostId 로 러너 게시글을 찾을 수 없습니다. runnerPostId 를 다시 확인해주세요")); + } + + public Long createRunnerPostApplicant(final Supporter supporter, + final RunnerPostApplicantCreateRequest request, + final Long runnerPostId + ) { + final RunnerPost foundRunnerPost = getRunnerPostOrThrowException(runnerPostId); + if (isApplySupporter(foundRunnerPost, supporter)) { + throw new RunnerPostBusinessException("Supporter 는 이미 해당 RunnerPost 에 리뷰 신청을 한 이력이 있습니다."); + } + + final SupporterRunnerPost runnerPostApplicant = SupporterRunnerPost.builder() + .supporter(supporter) + .runnerPost(foundRunnerPost) + .message(new Message(request.message())) + .build(); + + final Long savedApplicantId = supporterRunnerPostCommandRepository.save(runnerPostApplicant).getId(); + + eventPublisher.publishEvent(new RunnerPostApplySupporterEvent(foundRunnerPost.getId())); + + return savedApplicantId; + } + + public void updateRunnerPostReviewStatusDone(final Long runnerPostId, final Supporter supporter) { + final RunnerPost foundRunnerPost = runnerPostCommandRepository.findById(runnerPostId) + .orElseThrow(() -> new RunnerPostBusinessException("해당 식별자의 러너 게시글이 존재하지 않습니다.")); + + if (Objects.isNull(foundRunnerPost.getSupporter())) { + throw new RunnerPostBusinessException("아직 서포터가 배정이 안 된 게시글 입니다."); + } + + if (foundRunnerPost.isDifferentSupporter(supporter)) { + throw new RunnerPostBusinessException("다른 사람이 리뷰 중인 게시글의 상태를 변경할 수 없습니다."); + } + + foundRunnerPost.finishReview(); + + eventPublisher.publishEvent(new RunnerPostReviewStatusDoneEvent(foundRunnerPost.getId())); + } + + public void deleteSupporterRunnerPost(final Supporter supporter, final Long runnerPostId) { + final RunnerPost runnerPost = runnerPostCommandRepository.findById(runnerPostId) + .orElseThrow(() -> new RunnerPostBusinessException("존재하지 않는 RunnerPost 입니다.")); + if (!runnerPost.isReviewStatusNotStarted()) { + throw new RunnerPostBusinessException("이미 진행 중인 러너 게시글의 서포터 지원은 철회할 수 없습니다."); + } + supporterRunnerPostCommandRepository.deleteBySupporterIdAndRunnerPostId(supporter.getId(), runnerPostId); + } + + public void updateRunnerPostAppliedSupporter(final Runner runner, + final Long runnerPostId, + final RunnerPostUpdateRequest.SelectSupporter request + ) { + final Supporter foundApplySupporter = supporterCommandRepository.findById(request.supporterId()) + .orElseThrow(() -> new RunnerPostBusinessException("해당하는 식별자값의 서포터를 찾을 수 없습니다.")); + final RunnerPost foundRunnerPost = runnerPostCommandRepository.findById(runnerPostId) + .orElseThrow(() -> new RunnerPostBusinessException("RunnerPost 의 식별자값으로 러너 게시글을 조회할 수 없습니다.")); + + if (!isApplySupporter(foundRunnerPost, foundApplySupporter)) { + throw new RunnerPostBusinessException("게시글에 리뷰를 제안한 서포터가 아닙니다."); + } + if (foundRunnerPost.isNotOwner(runner)) { + throw new RunnerPostBusinessException("RunnerPost 의 글쓴이와 다른 사용자입니다."); + } + + foundRunnerPost.assignSupporter(foundApplySupporter); + + eventPublisher.publishEvent(new RunnerPostAssignSupporterEvent(foundRunnerPost.getId())); + } + + private boolean isApplySupporter(final RunnerPost runnerPost, final Supporter supporter) { + return supporterRunnerPostCommandRepository.existsByRunnerPostIdAndSupporterId(runnerPost.getId(), supporter.getId()); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/service/dto/RunnerPostApplicantCreateRequest.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/service/dto/RunnerPostApplicantCreateRequest.java new file mode 100644 index 000000000..99eceb52d --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/service/dto/RunnerPostApplicantCreateRequest.java @@ -0,0 +1,9 @@ +package touch.baton.domain.runnerpost.command.service.dto; + +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.runnerpost.command.exception.validator.ValidMaxLength; + +public record RunnerPostApplicantCreateRequest(@ValidMaxLength(clientErrorCode = ClientErrorCode.APPLICANT_MESSAGE_IS_OVERFLOW, max = 500) + String message +) { +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/service/dto/RunnerPostCreateRequest.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/service/dto/RunnerPostCreateRequest.java new file mode 100644 index 000000000..e26baee83 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/service/dto/RunnerPostCreateRequest.java @@ -0,0 +1,33 @@ +package touch.baton.domain.runnerpost.command.service.dto; + +import touch.baton.domain.common.exception.validator.ValidNotNull; +import touch.baton.domain.runnerpost.command.exception.validator.ValidFuture; +import touch.baton.domain.runnerpost.command.exception.validator.ValidMaxLength; +import touch.baton.domain.runnerpost.command.exception.validator.ValidNotUrl; + +import java.time.LocalDateTime; +import java.util.List; + +import static touch.baton.domain.common.exception.ClientErrorCode.*; + +public record RunnerPostCreateRequest(@ValidNotNull(clientErrorCode = TITLE_IS_NULL) + String title, + @ValidNotNull(clientErrorCode = TAGS_ARE_NULL) + List tags, + @ValidNotNull(clientErrorCode = PULL_REQUEST_URL_IS_NULL) + @ValidNotUrl(clientErrorCode = PULL_REQUEST_URL_IS_NOT_URL) + String pullRequestUrl, + @ValidNotNull(clientErrorCode = DEADLINE_IS_NULL) + @ValidFuture(clientErrorCode = PAST_DEADLINE) + LocalDateTime deadline, + @ValidNotNull(clientErrorCode = IMPLEMENTED_CONTENTS_ARE_NULL) + @ValidMaxLength(clientErrorCode = CONTENTS_OVERFLOW, max = 1000) + 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/command/service/dto/RunnerPostUpdateRequest.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/service/dto/RunnerPostUpdateRequest.java new file mode 100644 index 000000000..e4f528f36 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/service/dto/RunnerPostUpdateRequest.java @@ -0,0 +1,12 @@ +package touch.baton.domain.runnerpost.command.service.dto; + +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.validator.ValidNotNull; + +public record RunnerPostUpdateRequest() { + + 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/command/vo/CuriousContents.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/CuriousContents.java new file mode 100644 index 000000000..0f01a56a0 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/CuriousContents.java @@ -0,0 +1,32 @@ +package touch.baton.domain.runnerpost.command.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/command/vo/Deadline.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/Deadline.java new file mode 100644 index 000000000..2eba007dc --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/Deadline.java @@ -0,0 +1,38 @@ +package touch.baton.domain.runnerpost.command.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.Objects; + +import static java.time.temporal.ChronoUnit.MINUTES; +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class Deadline { + + @Column(name = "deadline", nullable = false) + private LocalDateTime value; + + public Deadline(final LocalDateTime value) { + validateNotNull(value); + this.value = value.truncatedTo(MINUTES); + } + + private void validateNotNull(final LocalDateTime value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("Deadline 객체 내부에 deadline 은 null 일 수 없습니다."); + } + } + + public boolean isEnd() { + return value.isBefore(LocalDateTime.now()); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/ImplementedContents.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/ImplementedContents.java new file mode 100644 index 000000000..bc55c11af --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/ImplementedContents.java @@ -0,0 +1,32 @@ +package touch.baton.domain.runnerpost.command.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/command/vo/IsReviewed.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/IsReviewed.java new file mode 100644 index 000000000..6870f9848 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/IsReviewed.java @@ -0,0 +1,35 @@ +package touch.baton.domain.runnerpost.command.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/command/vo/PostscriptContents.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/PostscriptContents.java new file mode 100644 index 000000000..e00074869 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/PostscriptContents.java @@ -0,0 +1,32 @@ +package touch.baton.domain.runnerpost.command.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/runnerpost/command/vo/PullRequestUrl.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/PullRequestUrl.java new file mode 100644 index 000000000..5dee19f74 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/PullRequestUrl.java @@ -0,0 +1,34 @@ +package touch.baton.domain.runnerpost.command.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 PullRequestUrl { + + private static final int MAXIMUM_URL_LENGTH = 2083; + + @Column(name = "pull_request_url", nullable = false, length = MAXIMUM_URL_LENGTH) + private String value; + + public PullRequestUrl(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("PullRequestUrl 객체 내부에 pull request url 은 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/ReviewStatus.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/ReviewStatus.java new file mode 100644 index 000000000..4a3109ac5 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/ReviewStatus.java @@ -0,0 +1,31 @@ +package touch.baton.domain.runnerpost.command.vo; + +import static java.util.Locale.ENGLISH; + +public enum ReviewStatus { + + NOT_STARTED, + IN_PROGRESS, + DONE, + OVERDUE; + + public static ReviewStatus from(final String name) { + return ReviewStatus.valueOf(name.toUpperCase(ENGLISH)); + } + + public boolean isSame(final ReviewStatus reviewStatus) { + return this == reviewStatus; + } + + public boolean isNotSameAsNotStarted() { + return this != NOT_STARTED; + } + + public boolean isOverdue() { + return this == OVERDUE; + } + + public boolean isNotStarted() { + return this == NOT_STARTED; + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/Title.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/Title.java new file mode 100644 index 000000000..0b890b4e5 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/Title.java @@ -0,0 +1,32 @@ +package touch.baton.domain.runnerpost.command.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 Title { + + @Column(name = "title", nullable = false) + private String value; + + public Title(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("Title 객체 내부에 title 은 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/WatchedCount.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/WatchedCount.java new file mode 100644 index 000000000..7aa370354 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/command/vo/WatchedCount.java @@ -0,0 +1,35 @@ +package touch.baton.domain.runnerpost.command.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnDefault; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class WatchedCount { + + private static final String DEFAULT_VALUE = "0"; + + @ColumnDefault(DEFAULT_VALUE) + @Column(name = "watched_count", nullable = false) + private int value; + + public WatchedCount(final int value) { + this.value = value; + } + + public static WatchedCount zero() { + return new WatchedCount(Integer.parseInt(DEFAULT_VALUE)); + } + + public WatchedCount increase() { + return new WatchedCount(value + 1); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/controller/RunnerPostQueryController.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/controller/RunnerPostQueryController.java new file mode 100644 index 000000000..8b2e376cd --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/controller/RunnerPostQueryController.java @@ -0,0 +1,128 @@ +package touch.baton.domain.runnerpost.query.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +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.request.PageParams; +import touch.baton.domain.common.response.PageResponse; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.oauth.query.controller.resolver.AuthMemberPrincipal; +import touch.baton.domain.oauth.query.controller.resolver.AuthRunnerPrincipal; +import touch.baton.domain.oauth.query.controller.resolver.AuthSupporterPrincipal; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.query.controller.response.RunnerPostResponse; +import touch.baton.domain.runnerpost.query.controller.response.SupporterRunnerPostResponse; +import touch.baton.domain.runnerpost.query.controller.response.SupporterRunnerPostResponses; +import touch.baton.domain.runnerpost.query.service.RunnerPostQueryService; + +import java.util.List; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/posts/runner") +@RestController +public class RunnerPostQueryController { + + private final RunnerPostQueryService runnerPostQueryService; + + @GetMapping + public ResponseEntity> readRunnerPostsByTagNameAndReviewStatus( + @Valid @ModelAttribute final PageParams pageParams, + @RequestParam(required = false) final String tagName, + @RequestParam(required = false) final ReviewStatus reviewStatus + ) { + return ResponseEntity.ok(runnerPostQueryService.pageRunnerPostByTagNameAndReviewStatus(tagName, pageParams, reviewStatus)); + } + + @GetMapping("/{runnerPostId}") + public ResponseEntity readByRunnerPostId( + @AuthMemberPrincipal(required = false) final Member member, + @PathVariable final Long runnerPostId + ) { + final RunnerPost foundRunnerPost = runnerPostQueryService.readByRunnerPostId(runnerPostId); + final long applicantCount = runnerPostQueryService.countApplicantsByRunnerPostId(foundRunnerPost.getId()); + final boolean isApplicantHistoryExist = runnerPostQueryService.existsRunnerPostApplicantByRunnerPostIdAndMemberId(runnerPostId, member.getId()); + + runnerPostQueryService.increaseWatchedCount(foundRunnerPost); + final RunnerPostResponse.Detail response = RunnerPostResponse.Detail.of( + foundRunnerPost, + foundRunnerPost.isOwner(member), + isApplicantHistoryExist, + applicantCount + ); + + return ResponseEntity.ok(response); + } + + @GetMapping("/search") + public ResponseEntity> readRunnerPostBySupporterIdAndReviewStatusDone( + @Valid @ModelAttribute final PageParams pageParams, + @RequestParam final Long supporterId + ) { + return ResponseEntity.ok(runnerPostQueryService.pageRunnerPostBySupporterIdAndReviewStatus(pageParams, supporterId, ReviewStatus.DONE)); + } + + @GetMapping("/search/count") + public ResponseEntity countRunnerPostBySupporterIdAndReviewStatusDone( + @RequestParam final Long supporterId + ) { + final long count = runnerPostQueryService.countRunnerPostBySupporterIdAndReviewStatus(supporterId, ReviewStatus.DONE); + return ResponseEntity.ok(RunnerPostResponse.Count.from(count)); + } + + @GetMapping("/{runnerPostId}/supporters") + public ResponseEntity readSupporterRunnerPostsByRunnerPostId( + @AuthRunnerPrincipal final Runner runner, + @PathVariable final Long runnerPostId + ) { + final List responses = runnerPostQueryService.readSupporterRunnerPostsByRunnerPostId(runner, runnerPostId).stream() + .map(SupporterRunnerPostResponse.Detail::from) + .toList(); + + return ResponseEntity.ok(SupporterRunnerPostResponses.Detail.from(responses)); + } + + @GetMapping("/me/supporter") + public ResponseEntity> readRunnerPostByLoginedSupporterAndReviewStatus( + @AuthSupporterPrincipal final Supporter supporter, + @Valid @ModelAttribute final PageParams pageParams, + @RequestParam(required = false) final ReviewStatus reviewStatus + ) { + return ResponseEntity.ok(runnerPostQueryService.pageRunnerPostBySupporterIdAndReviewStatus(pageParams, supporter.getId(), reviewStatus)); + } + + @GetMapping("/me/runner") + public ResponseEntity> readRunnerPostByLoginedRunnerAndReviewStatus( + @AuthRunnerPrincipal final Runner runner, + @Valid @ModelAttribute final PageParams pageParams, + @RequestParam(required = false) final ReviewStatus reviewStatus + ) { + return ResponseEntity.ok(runnerPostQueryService.pageRunnerPostByRunnerIdAndReviewStatus(pageParams, runner.getId(), reviewStatus)); + } + + @GetMapping("/me/supporter/count") + public ResponseEntity countRunnerPostByLoginedSupporterAndReviewStatus( + @AuthSupporterPrincipal final Supporter supporter, + @RequestParam final ReviewStatus reviewStatus + ) { + final long runnerPostCount = runnerPostQueryService.countRunnerPostBySupporterIdAndReviewStatus(supporter.getId(), reviewStatus); + return ResponseEntity.ok(RunnerPostResponse.Count.from(runnerPostCount)); + } + + @GetMapping("/me/runner/count") + public ResponseEntity countRunnerPostByLoginedRunnerAndReviewStatus( + @AuthRunnerPrincipal final Runner runner, + @RequestParam final ReviewStatus reviewStatus + ) { + final long runnerPostCount = runnerPostQueryService.countRunnerPostByRunnerIdAndReviewStatus(runner.getId(), reviewStatus); + return ResponseEntity.ok(RunnerPostResponse.Count.from(runnerPostCount)); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/controller/response/RunnerPostResponse.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/controller/response/RunnerPostResponse.java new file mode 100644 index 000000000..b6be711c4 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/controller/response/RunnerPostResponse.java @@ -0,0 +1,150 @@ +package touch.baton.domain.runnerpost.query.controller.response; + +import touch.baton.domain.common.response.IdExtractable; +import touch.baton.domain.member.query.controller.response.RunnerResponse; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.tag.command.RunnerPostTag; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; + +public record RunnerPostResponse() { + + public record Detail(Long runnerPostId, + String title, + String implementedContents, + String curiousContents, + String postscriptContents, + String pullRequestUrl, + LocalDateTime deadline, + int watchedCount, + long applicantCount, + ReviewStatus reviewStatus, + boolean isOwner, + boolean isApplied, + List tags, + RunnerResponse.InRunnerPostDetail runnerProfile + ) { + + public static Detail of(final RunnerPost runnerPost, + final boolean isOwner, + final boolean isApplied, + final long applicantCount + ) { + return new Detail( + runnerPost.getId(), + runnerPost.getTitle().getValue(), + runnerPost.getImplementedContents().getValue(), + runnerPost.getCuriousContents().getValue(), + runnerPost.getPostscriptContents().getValue(), + runnerPost.getPullRequestUrl().getValue(), + runnerPost.getDeadline().getValue(), + runnerPost.getWatchedCount().getValue(), + applicantCount, + runnerPost.getReviewStatus(), + isOwner, + isApplied, + convertToTags(runnerPost), + RunnerResponse.InRunnerPostDetail.from(runnerPost.getRunner()) + ); + } + } + + public record Simple(Long runnerPostId, + String title, + LocalDateTime deadline, + int watchedCount, + long applicantCount, + String reviewStatus, + RunnerResponse.Simple runnerProfile, + List tags + ) implements IdExtractable { + + public static Simple of(final RunnerPost runnerPost, + final long applicantCount, + final List runnerPostTags + ) { + return new Simple( + runnerPost.getId(), + runnerPost.getTitle().getValue(), + runnerPost.getDeadline().getValue(), + runnerPost.getWatchedCount().getValue(), + applicantCount, + runnerPost.getReviewStatus().name(), + RunnerResponse.Simple.from(runnerPost.getRunner()), + convertToTags(runnerPost, runnerPostTags) + ); + } + + @Override + public Long extractId() { + return runnerPostId; + } + } + + public record SimpleByRunner(Long runnerPostId, + String title, + LocalDateTime deadline, + int watchedCount, + long applicantCount, + String reviewStatus, + boolean isReviewed, + Long supporterId, + List tags + + ) implements IdExtractable { + + public static SimpleByRunner of(final RunnerPost runnerPost, + final long applicantCount, + final List runnerPostTags + ) { + return new SimpleByRunner( + runnerPost.getId(), + runnerPost.getTitle().getValue(), + runnerPost.getDeadline().getValue(), + runnerPost.getWatchedCount().getValue(), + applicantCount, + runnerPost.getReviewStatus().name(), + runnerPost.getIsReviewed().getValue(), + getSupporterIdByRunnerPost(runnerPost), + convertToTags(runnerPost, runnerPostTags) + ); + } + + private static Long getSupporterIdByRunnerPost(final RunnerPost runnerPost) { + if (Objects.isNull(runnerPost.getSupporter())) { + return null; + } + return runnerPost.getSupporter().getId(); + } + + @Override + public Long extractId() { + return runnerPostId; + } + } + + public record Count(Long count) { + + public static Count from(final long count) { + return new Count(count); + } + } + + private static List convertToTags(final RunnerPost runnerPost) { + return runnerPost.getRunnerPostTags() + .getRunnerPostTags() + .stream() + .map(runnerPostTag -> runnerPostTag.getTag().getTagName().getValue()) + .toList(); + } + + private static List convertToTags(final RunnerPost runnerPost, final List runnerPostTags) { + return runnerPostTags.stream() + .filter(runnerPostTag -> Objects.equals(runnerPostTag.getRunnerPost().getId(), runnerPost.getId())) + .map(runnerPostTag -> runnerPostTag.getTag().getTagName().getValue()) + .toList(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/controller/response/SupporterRunnerPostResponse.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/controller/response/SupporterRunnerPostResponse.java new file mode 100644 index 000000000..22f270c5c --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/controller/response/SupporterRunnerPostResponse.java @@ -0,0 +1,37 @@ +package touch.baton.domain.runnerpost.query.controller.response; + +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.SupporterRunnerPost; + +import java.util.List; + +public record SupporterRunnerPostResponse() { + + public record Detail(Long supporterId, + String name, + String company, + int reviewCount, + String imageUrl, + String message, + List technicalTags + ) { + + public static Detail from(final SupporterRunnerPost supporterRunnerPost) { + return new Detail( + supporterRunnerPost.getSupporter().getId(), + supporterRunnerPost.getSupporter().getMember().getMemberName().getValue(), + supporterRunnerPost.getSupporter().getMember().getCompany().getValue(), + supporterRunnerPost.getSupporter().getReviewCount().getValue(), + supporterRunnerPost.getSupporter().getMember().getImageUrl().getValue(), + supporterRunnerPost.getMessage().getValue(), + convertToTechnicalTags(supporterRunnerPost.getSupporter()) + ); + } + + private static List convertToTechnicalTags(final Supporter supporter) { + return supporter.getSupporterTechnicalTags().getSupporterTechnicalTags().stream() + .map(supporterTechnicalTag -> supporterTechnicalTag.getTechnicalTag().getTagName().getValue()) + .toList(); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/controller/response/SupporterRunnerPostResponses.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/controller/response/SupporterRunnerPostResponses.java new file mode 100644 index 000000000..c84d4894a --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/controller/response/SupporterRunnerPostResponses.java @@ -0,0 +1,13 @@ +package touch.baton.domain.runnerpost.query.controller.response; + +import java.util.List; + +public record SupporterRunnerPostResponses() { + + 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/runnerpost/query/repository/RunnerPostPageRepository.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/repository/RunnerPostPageRepository.java new file mode 100644 index 000000000..d0ac63181 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/repository/RunnerPostPageRepository.java @@ -0,0 +1,123 @@ +package touch.baton.domain.runnerpost.query.repository; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.tag.command.RunnerPostTag; +import touch.baton.domain.tag.command.vo.TagReducedName; + +import java.util.List; + +import static touch.baton.domain.member.command.QMember.member; +import static touch.baton.domain.member.command.QRunner.runner; +import static touch.baton.domain.member.command.QSupporter.supporter; +import static touch.baton.domain.member.command.QSupporterRunnerPost.supporterRunnerPost; +import static touch.baton.domain.runnerpost.command.QRunnerPost.runnerPost; +import static touch.baton.domain.tag.command.QRunnerPostTag.runnerPostTag; +import static touch.baton.domain.tag.command.QTag.tag; + +@RequiredArgsConstructor +@Repository +public class RunnerPostPageRepository { + + private final JPAQueryFactory jpaQueryFactory; + + public List pageByReviewStatusAndTagReducedName(final Long previousLastId, + final int limit, + final TagReducedName tagReducedName, + final ReviewStatus reviewStatus + ) { + final JPAQuery query = jpaQueryFactory.selectFrom(runnerPost) + .join(runnerPost.runner, runner).fetchJoin() + .join(runner.member, member).fetchJoin() + .where(previousLastIdLt(previousLastId), reviewStatusEq(reviewStatus)) + .orderBy(runnerPost.id.desc()) + .limit(limit); + + if (tagReducedName != null) { + query.leftJoin(runnerPost.runnerPostTags.runnerPostTags, runnerPostTag) + .leftJoin(runnerPostTag.tag, tag) + .where(tag.tagReducedName.eq(tagReducedName)); + } + + return query.fetch(); + } + + public List pageBySupporterIdAndReviewStatus(final Long previousLastId, + final int limit, + final Long supporterId, + final ReviewStatus reviewStatus + ) { + return jpaQueryFactory.selectFrom(runnerPost) + .join(runnerPost.runner, runner).fetchJoin() + .join(runner.member, member).fetchJoin() + .join(runnerPost.supporter, supporter).fetchJoin() + .where(previousLastIdLt(previousLastId), supporterIdEq(supporterId), reviewStatusEq(reviewStatus)) + .orderBy(runnerPost.id.desc()) + .limit(limit) + .fetch(); + } + + public List pageBySupporterIdAndReviewStatusNotStarted(final Long previousLastId, final int limit, final Long supporterId) { + return jpaQueryFactory.select(supporterRunnerPost.runnerPost) + .from(supporterRunnerPost) + .join(supporterRunnerPost.runnerPost, runnerPost) + .join(runnerPost.runner, runner).fetchJoin() + .join(runner.member, member).fetchJoin() + .where(previousLastIdLt(previousLastId), supporterIdEqInSupporterRunnerPost(supporterId), reviewStatusEq(ReviewStatus.NOT_STARTED)) + .orderBy(runnerPost.id.desc()) + .limit(limit) + .fetch(); + } + + public List pageByRunnerIdAndReviewStatus(final Long previousLastId, + final int limit, + final Long runnerId, + final ReviewStatus reviewStatus + ) { + return jpaQueryFactory.selectFrom(runnerPost) + .join(runnerPost.runner, runner).fetchJoin() + .join(runner.member, member).fetchJoin() + .where(previousLastIdLt(previousLastId), runnerIdEq(runnerId), reviewStatusEq(reviewStatus)) + .orderBy(runnerPost.id.desc()) + .limit(limit) + .fetch(); + } + + private BooleanExpression previousLastIdLt(final Long previousLastId) { + if (previousLastId == null) { + return null; + } + return runnerPost.id.lt(previousLastId); + } + + private BooleanExpression reviewStatusEq(final ReviewStatus reviewStatus) { + if (reviewStatus == null) { + return null; + } + return runnerPost.reviewStatus.eq(reviewStatus); + } + + private BooleanExpression supporterIdEq(final Long supporterId) { + return runnerPost.supporter.id.eq(supporterId); + } + + private BooleanExpression runnerIdEq(final Long runnerId) { + return runnerPost.runner.id.eq(runnerId); + } + + private BooleanExpression supporterIdEqInSupporterRunnerPost(final Long supporterId) { + return supporterRunnerPost.supporter.id.eq(supporterId); + } + + public List findRunnerPostTagsByRunnerPosts(final List runnerPosts) { + return jpaQueryFactory.selectFrom(runnerPostTag) + .join(runnerPostTag.tag, tag).fetchJoin() + .where(runnerPostTag.runnerPost.in(runnerPosts)) + .fetch(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/repository/RunnerPostQueryRepository.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/repository/RunnerPostQueryRepository.java new file mode 100644 index 000000000..962577ca5 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/repository/RunnerPostQueryRepository.java @@ -0,0 +1,57 @@ +package touch.baton.domain.runnerpost.query.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.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.repository.dto.RunnerPostApplicantCountDto; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; + +import java.util.List; +import java.util.Optional; + +public interface RunnerPostQueryRepository extends JpaRepository { + + @Query(value = """ + 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 + where rp.id = :runnerPostId + """) + Optional joinMemberByRunnerPostId(@Param("runnerPostId") final Long runnerPostId); + + @Query(""" + select rp + from RunnerPost rp + join fetch Supporter s on s.id = rp.supporter.id + join fetch Member m on m.id = s.member.id + where rp.id = :runnerPostId + """) + Optional joinSupporterByRunnerPostId(@Param("runnerPostId") final Long runnerPostId); + + List findByRunnerId(final Long runnerId); + + @Query(""" + select new touch.baton.domain.runnerpost.command.repository.dto.RunnerPostApplicantCountDto(rp.id, coalesce(count(srp.id), 0L)) + from RunnerPost rp + left join 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 count(1) + from RunnerPost rp + where rp.runner.id = :runnerId and rp.reviewStatus = :reviewStatus + """) + long countByRunnerIdAndReviewStatus(@Param("runnerId") final Long runnerId, @Param("reviewStatus") final ReviewStatus reviewStatus); + + @Query(""" + select count(1) + from RunnerPost rp + where rp.supporter.id = :supporterId and rp.reviewStatus = :reviewStatus + """) + long countBySupporterIdAndReviewStatus(@Param("supporterId") final Long supporterId, @Param("reviewStatus") final ReviewStatus reviewStatus); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/repository/dto/ApplicantCountMappingDto.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/repository/dto/ApplicantCountMappingDto.java new file mode 100644 index 000000000..172684823 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/repository/dto/ApplicantCountMappingDto.java @@ -0,0 +1,10 @@ +package touch.baton.domain.runnerpost.query.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/query/service/RunnerPostQueryService.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/service/RunnerPostQueryService.java new file mode 100644 index 000000000..e711fea85 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/query/service/RunnerPostQueryService.java @@ -0,0 +1,136 @@ +package touch.baton.domain.runnerpost.query.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import touch.baton.domain.common.request.PageParams; +import touch.baton.domain.common.response.PageResponse; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.SupporterRunnerPost; +import touch.baton.domain.member.query.repository.SupporterRunnerPostQueryRepository; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.RunnerPostsApplicantCount; +import touch.baton.domain.runnerpost.command.exception.RunnerPostBusinessException; +import touch.baton.domain.runnerpost.command.repository.dto.RunnerPostApplicantCountDto; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.query.controller.response.RunnerPostResponse; +import touch.baton.domain.runnerpost.query.repository.RunnerPostPageRepository; +import touch.baton.domain.runnerpost.query.repository.RunnerPostQueryRepository; +import touch.baton.domain.tag.command.RunnerPostTag; +import touch.baton.domain.tag.command.vo.TagReducedName; +import touch.baton.domain.tag.query.repository.RunnerPostTagQueryRepository; + +import java.util.List; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class RunnerPostQueryService { + + private final RunnerPostQueryRepository runnerPostQueryRepository; + private final RunnerPostPageRepository runnerPostPageRepository; + private final RunnerPostTagQueryRepository runnerPostTagQueryRepository; + private final SupporterRunnerPostQueryRepository supporterRunnerPostQueryRepository; + + public RunnerPost readByRunnerPostId(final Long runnerPostId) { + runnerPostTagQueryRepository.joinTagByRunnerPostId(runnerPostId); + return runnerPostQueryRepository.joinMemberByRunnerPostId(runnerPostId) + .orElseThrow(() -> new RunnerPostBusinessException("RunnerPost 의 식별자값으로 러너 게시글을 조회할 수 없습니다.")); + } + + public PageResponse pageRunnerPostByTagNameAndReviewStatus(final String tagName, + final PageParams pageParams, + final ReviewStatus reviewStatus + ) { + final List runnerPosts = runnerPostPageRepository.pageByReviewStatusAndTagReducedName(pageParams.cursor(), pageParams.getLimitForQuery(), TagReducedName.nullableInstance(tagName), reviewStatus); + final List runnerPostTags = runnerPostPageRepository.findRunnerPostTagsByRunnerPosts(runnerPosts); + final RunnerPostsApplicantCount runnerPostsApplicantCount = readRunnerPostsApplicantCount(runnerPosts); + final List responses = runnerPosts.stream() + .map(runnerPost -> RunnerPostResponse.Simple.of(runnerPost, runnerPostsApplicantCount.getApplicantCountById(runnerPost.getId()), runnerPostTags)) + .toList(); + return PageResponse.of(responses, pageParams); + } + + public PageResponse pageRunnerPostBySupporterIdAndReviewStatus(final PageParams pageParams, + final Long supporterId, + final ReviewStatus reviewStatus + ) { + final List runnerPosts = pageRunnerPostFromSupporterByReviewStatus(pageParams, supporterId, reviewStatus); + final List runnerPostTags = runnerPostPageRepository.findRunnerPostTagsByRunnerPosts(runnerPosts); + final RunnerPostsApplicantCount runnerPostsApplicantCount = readRunnerPostsApplicantCount(runnerPosts); + final List responses = runnerPosts.stream() + .map(runnerPost -> RunnerPostResponse.Simple.of(runnerPost, runnerPostsApplicantCount.getApplicantCountById(runnerPost.getId()), runnerPostTags)) + .toList(); + return PageResponse.of(responses, pageParams); + } + + private List pageRunnerPostFromSupporterByReviewStatus(final PageParams pageParams, + final Long supporterId, + final ReviewStatus reviewStatus + ) { + if (reviewStatus == ReviewStatus.NOT_STARTED) { + return runnerPostPageRepository.pageBySupporterIdAndReviewStatusNotStarted(pageParams.cursor(), pageParams.getLimitForQuery(), supporterId); + } + return runnerPostPageRepository.pageBySupporterIdAndReviewStatus(pageParams.cursor(), pageParams.getLimitForQuery(), supporterId, reviewStatus); + } + + private RunnerPostsApplicantCount readRunnerPostsApplicantCount(final List runnerPosts) { + final List runnerPostIds = runnerPosts.stream() + .map(RunnerPost::getId) + .toList(); + final List runnerPostApplicantCountDtos = runnerPostQueryRepository.countApplicantsByRunnerPostIds(runnerPostIds); + return RunnerPostsApplicantCount.from(runnerPostApplicantCountDtos); + } + + public PageResponse pageRunnerPostByRunnerIdAndReviewStatus(final PageParams pageParams, + final Long runnerId, + final ReviewStatus reviewStatus + ) { + final List runnerPosts = runnerPostPageRepository.pageByRunnerIdAndReviewStatus(pageParams.cursor(), pageParams.getLimitForQuery(), runnerId, reviewStatus); + final List runnerPostTags = runnerPostPageRepository.findRunnerPostTagsByRunnerPosts(runnerPosts); + final RunnerPostsApplicantCount runnerPostsApplicantCount = readRunnerPostsApplicantCount(runnerPosts); + final List responses = runnerPosts.stream() + .map(runnerPost -> RunnerPostResponse.SimpleByRunner.of(runnerPost, runnerPostsApplicantCount.getApplicantCountById(runnerPost.getId()), runnerPostTags)) + .toList(); + return PageResponse.of(responses, pageParams); + } + + public List readSupporterRunnerPostsByRunnerPostId(final Runner runner, final Long runnerPostId) { + final RunnerPost foundRunnerPost = runnerPostQueryRepository.joinMemberByRunnerPostId(runnerPostId) + .orElseThrow(() -> new RunnerPostBusinessException(("RunnerPost 의 식별자값으로 러너 게시글을 조회할 수 없습니다."))); + + if (foundRunnerPost.isNotOwner(runner)) { + throw new RunnerPostBusinessException("RunnerPost 의 작성자가 아닙니다."); + } + + return supporterRunnerPostQueryRepository.readByRunnerPostId(runnerPostId); + } + + public List readRunnerPostsByRunnerId(final Long runnerId) { + return runnerPostQueryRepository.findByRunnerId(runnerId); + } + + public boolean existsRunnerPostApplicantByRunnerPostIdAndMemberId(final Long runnerPostId, final Long memberId) { + return supporterRunnerPostQueryRepository.existsByRunnerPostIdAndMemberId(runnerPostId, memberId); + } + + public long countApplicantsByRunnerPostId(final Long runnerPostId) { + return supporterRunnerPostQueryRepository.countByRunnerPostId(runnerPostId).orElse(0L); + } + + public long countRunnerPostByRunnerIdAndReviewStatus(final Long runnerId, final ReviewStatus reviewStatus) { + return runnerPostQueryRepository.countByRunnerIdAndReviewStatus(runnerId, reviewStatus); + } + + public long countRunnerPostBySupporterIdAndReviewStatus(final Long supporterId, final ReviewStatus reviewStatus) { + if (reviewStatus.isNotStarted()) { + return supporterRunnerPostQueryRepository.countRunnerPostBySupporterIdByReviewStatusNotStarted(supporterId); + } + return runnerPostQueryRepository.countBySupporterIdAndReviewStatus(supporterId, reviewStatus); + } + + @Transactional + public void increaseWatchedCount(final RunnerPost runnerPost) { + runnerPost.increaseWatchedCount(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/command/RunnerPostTag.java b/backend/baton/src/main/java/touch/baton/domain/tag/command/RunnerPostTag.java new file mode 100644 index 000000000..43372823a --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/command/RunnerPostTag.java @@ -0,0 +1,61 @@ +package touch.baton.domain.tag.command; + +import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.tag.exception.RunnerPostTagDomainException; + +import java.util.Objects; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class RunnerPostTag { + + @GeneratedValue(strategy = IDENTITY) + @Id + private Long id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "runner_post_id", + nullable = false, + foreignKey = @ForeignKey(name = "fk_runner_post_tag_to_runner_post")) + private RunnerPost runnerPost; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "tag_id", nullable = false, foreignKey = @ForeignKey(name = "fk_runner_post_tag_to_tag")) + private Tag tag; + + @Builder + private RunnerPostTag(final RunnerPost runnerPost, final Tag tag) { + this(null, runnerPost, tag); + } + + private RunnerPostTag(final Long id, final RunnerPost runnerPost, final Tag tag) { + validateNotNull(runnerPost, tag); + this.id = id; + this.runnerPost = runnerPost; + this.tag = tag; + } + + private void validateNotNull(final RunnerPost runnerPost, final Tag tag) { + if (Objects.isNull(runnerPost)) { + throw new RunnerPostTagDomainException("RunnerPostTag 의 runnerPost 는 null 일 수 없습니다."); + } + + if (Objects.isNull(tag)) { + throw new RunnerPostTagDomainException("RunnerPostTag 의 tag 는 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/command/RunnerPostTags.java b/backend/baton/src/main/java/touch/baton/domain/tag/command/RunnerPostTags.java new file mode 100644 index 000000000..8d97a9272 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/command/RunnerPostTags.java @@ -0,0 +1,49 @@ +package touch.baton.domain.tag.command; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.OneToMany; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.BatchSize; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static jakarta.persistence.CascadeType.PERSIST; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class RunnerPostTags { + + @BatchSize(size = 5) + @OneToMany(mappedBy = "runnerPost", cascade = PERSIST, orphanRemoval = true) + private List runnerPostTags = new ArrayList<>(); + + public RunnerPostTags(final List runnerPostTags) { + this.runnerPostTags = runnerPostTags; + } + + public void add(final RunnerPostTag runnerPostTag) { + runnerPostTags.add(runnerPostTag); + } + + public void addAll(final List runnerPostTags) { + this.runnerPostTags.addAll(runnerPostTags); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final RunnerPostTags that = (RunnerPostTags) o; + return Objects.equals(runnerPostTags, that.runnerPostTags); + } + + @Override + public int hashCode() { + return Objects.hash(runnerPostTags); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/command/Tag.java b/backend/baton/src/main/java/touch/baton/domain/tag/command/Tag.java new file mode 100644 index 000000000..bcdd5cc63 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/command/Tag.java @@ -0,0 +1,61 @@ +package touch.baton.domain.tag.command; + +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.tag.command.vo.TagReducedName; +import touch.baton.domain.tag.exception.TagDomainException; + +import java.util.Objects; + +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class Tag { + + @GeneratedValue(strategy = IDENTITY) + @Id + private Long id; + + @Embedded + private TagName tagName; + + @Embedded + private TagReducedName tagReducedName; + + @Builder + private Tag(final TagName tagName, final TagReducedName tagReducedName) { + this(null, tagName, tagReducedName); + } + + 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, 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(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/command/repository/TagCommandRepository.java b/backend/baton/src/main/java/touch/baton/domain/tag/command/repository/TagCommandRepository.java new file mode 100644 index 000000000..187d4b72a --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/command/repository/TagCommandRepository.java @@ -0,0 +1,12 @@ +package touch.baton.domain.tag.command.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.tag.command.Tag; + +import java.util.Optional; + +public interface TagCommandRepository extends JpaRepository { + + Optional findByTagName(final TagName tagName); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/command/vo/TagReducedName.java b/backend/baton/src/main/java/touch/baton/domain/tag/command/vo/TagReducedName.java new file mode 100644 index 000000000..dfb09ae77 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/command/vo/TagReducedName.java @@ -0,0 +1,53 @@ +package touch.baton.domain.tag.command.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 nullableInstance(final String notReducedValue) { + if (notReducedValue == null) { + return null; + } + return new TagReducedName(reduceName(notReducedValue)); + } + + 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/domain/tag/query/controller/TagQueryController.java b/backend/baton/src/main/java/touch/baton/domain/tag/query/controller/TagQueryController.java new file mode 100644 index 000000000..d94506f47 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/query/controller/TagQueryController.java @@ -0,0 +1,31 @@ +package touch.baton.domain.tag.query.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.command.Tag; +import touch.baton.domain.tag.command.vo.TagReducedName; +import touch.baton.domain.tag.query.controller.response.TagSearchResponses; +import touch.baton.domain.tag.query.service.TagQueryService; + +import java.util.List; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/tags") +@RestController +public class TagQueryController { + + private final TagQueryService tagQueryService; + + @GetMapping("/search") + public ResponseEntity readTagsByTagName(@Nullable @RequestParam(required = false) final String tagName) { + final TagReducedName tagReducedName = TagReducedName.nullableInstance(tagName); + final List foundTags = tagQueryService.readTagsByReducedName(tagReducedName, 10); + + return ResponseEntity.ok(TagSearchResponses.Detail.from(foundTags)); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/query/controller/response/TagSearchResponse.java b/backend/baton/src/main/java/touch/baton/domain/tag/query/controller/response/TagSearchResponse.java new file mode 100644 index 000000000..c06017a2c --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/query/controller/response/TagSearchResponse.java @@ -0,0 +1,12 @@ +package touch.baton.domain.tag.query.controller.response; + +import touch.baton.domain.tag.command.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/query/controller/response/TagSearchResponses.java b/backend/baton/src/main/java/touch/baton/domain/tag/query/controller/response/TagSearchResponses.java new file mode 100644 index 000000000..5bd9be9ba --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/query/controller/response/TagSearchResponses.java @@ -0,0 +1,19 @@ +package touch.baton.domain.tag.query.controller.response; + +import touch.baton.domain.tag.command.Tag; + +import java.util.List; + +public record TagSearchResponses() { + + public record Detail(List data) { + + public static Detail from(final List tags) { + final List response = tags.stream() + .map(TagSearchResponse.TagResponse::from) + .toList(); + + return new Detail(response); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/query/repository/RunnerPostTagQueryRepository.java b/backend/baton/src/main/java/touch/baton/domain/tag/query/repository/RunnerPostTagQueryRepository.java new file mode 100644 index 000000000..bcf258a6c --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/query/repository/RunnerPostTagQueryRepository.java @@ -0,0 +1,19 @@ +package touch.baton.domain.tag.query.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.tag.command.RunnerPostTag; + +import java.util.List; + +public interface RunnerPostTagQueryRepository extends JpaRepository { + + @Query(""" + select rpt + from RunnerPostTag rpt + join fetch Tag tag on rpt.tag.id = tag.id + where rpt.runnerPost.id = :runnerPostId + """) + List joinTagByRunnerPostId(@Param("runnerPostId") final Long runnerPostId); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/query/repository/TagQuerydslRepository.java b/backend/baton/src/main/java/touch/baton/domain/tag/query/repository/TagQuerydslRepository.java new file mode 100644 index 000000000..d1e74cd8b --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/query/repository/TagQuerydslRepository.java @@ -0,0 +1,26 @@ +package touch.baton.domain.tag.query.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import touch.baton.domain.tag.command.Tag; +import touch.baton.domain.tag.command.vo.TagReducedName; + +import java.util.List; + +import static touch.baton.domain.tag.command.QTag.tag; + +@RequiredArgsConstructor +@Repository +public class TagQuerydslRepository { + + private final JPAQueryFactory queryFactory; + + public List findByTagReducedName(final TagReducedName tagReducedName, final int limit) { + return queryFactory.selectFrom(tag) + .where(tag.tagReducedName.value.startsWith(tagReducedName.getValue())) + .orderBy(tag.tagReducedName.value.asc(), tag.tagName.value.desc()) + .limit(limit) + .fetch(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/query/service/TagQueryService.java b/backend/baton/src/main/java/touch/baton/domain/tag/query/service/TagQueryService.java new file mode 100644 index 000000000..1fcee1ae1 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/query/service/TagQueryService.java @@ -0,0 +1,27 @@ +package touch.baton.domain.tag.query.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import touch.baton.domain.tag.command.Tag; +import touch.baton.domain.tag.command.vo.TagReducedName; +import touch.baton.domain.tag.query.repository.TagQuerydslRepository; + +import java.util.Collections; +import java.util.List; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class TagQueryService { + + private final TagQuerydslRepository tagQuerydslRepository; + + public List readTagsByReducedName(final TagReducedName tagReducedName, final int limit) { + if (tagReducedName == null || tagReducedName.getValue().isBlank()) { + return Collections.emptyList(); + } + + return tagQuerydslRepository.findByTagReducedName(tagReducedName, limit); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/technicaltag/command/RunnerTechnicalTag.java b/backend/baton/src/main/java/touch/baton/domain/technicaltag/command/RunnerTechnicalTag.java new file mode 100644 index 000000000..8cb877e24 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/technicaltag/command/RunnerTechnicalTag.java @@ -0,0 +1,63 @@ +package touch.baton.domain.technicaltag.command; + +import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.tag.exception.SupporterTechnicalTagDomainException; + +import java.util.Objects; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class RunnerTechnicalTag { + + @GeneratedValue(strategy = IDENTITY) + @Id + private Long id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "runner_id", + nullable = false, + foreignKey = @ForeignKey(name = "fk_runner_technical_tag_to_runner")) + private Runner runner; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "technical_tag_id", + nullable = false, + foreignKey = @ForeignKey(name = "fk_runner_technical_tag_to_technical_tag")) + private TechnicalTag technicalTag; + + @Builder + private RunnerTechnicalTag(final Runner runner, final TechnicalTag technicalTag) { + this(null, runner, technicalTag); + } + + private RunnerTechnicalTag(final Long id, final Runner runner, final TechnicalTag technicalTag) { + validateNotNull(runner, technicalTag); + this.id = id; + this.runner = runner; + this.technicalTag = technicalTag; + } + + private void validateNotNull(final Runner runner, final TechnicalTag technicalTag) { + if (Objects.isNull(runner)) { + throw new SupporterTechnicalTagDomainException("RunnerTechnicalTag 의 runner 는 null 일 수 없습니다."); + } + + if (Objects.isNull(technicalTag)) { + throw new SupporterTechnicalTagDomainException("RunnerTechnicalTag 의 technicalTag 는 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/technicaltag/command/RunnerTechnicalTags.java b/backend/baton/src/main/java/touch/baton/domain/technicaltag/command/RunnerTechnicalTags.java new file mode 100644 index 000000000..6cac0afd6 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/technicaltag/command/RunnerTechnicalTags.java @@ -0,0 +1,29 @@ +package touch.baton.domain.technicaltag.command; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.OneToMany; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +import static jakarta.persistence.CascadeType.PERSIST; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class RunnerTechnicalTags { + + @OneToMany(mappedBy = "runner", cascade = PERSIST, orphanRemoval = true) + private List runnerTechnicalTags = new ArrayList<>(); + + public RunnerTechnicalTags(final List runnerTechnicalTags) { + this.runnerTechnicalTags = runnerTechnicalTags; + } + + public void addAll(final List runnerTechnicalTags) { + this.runnerTechnicalTags.addAll(runnerTechnicalTags); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/technicaltag/command/SupporterTechnicalTag.java b/backend/baton/src/main/java/touch/baton/domain/technicaltag/command/SupporterTechnicalTag.java new file mode 100644 index 000000000..c19748a9c --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/technicaltag/command/SupporterTechnicalTag.java @@ -0,0 +1,63 @@ +package touch.baton.domain.technicaltag.command; + +import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.tag.exception.SupporterTechnicalTagDomainException; + +import java.util.Objects; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class SupporterTechnicalTag { + + @GeneratedValue(strategy = IDENTITY) + @Id + private Long id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "supporter_id", + nullable = false, + foreignKey = @ForeignKey(name = "fk_supporter_technical_tag_to_supporter")) + private Supporter supporter; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "technical_tag_id", + nullable = false, + foreignKey = @ForeignKey(name = "fk_supporter_technical_tag_to_technical_tag")) + private TechnicalTag technicalTag; + + @Builder + private SupporterTechnicalTag(final Supporter supporter, final TechnicalTag technicalTag) { + this(null, supporter, technicalTag); + } + + private SupporterTechnicalTag(final Long id, final Supporter supporter, final TechnicalTag technicalTag) { + validateNotNull(supporter, technicalTag); + this.id = id; + this.supporter = supporter; + this.technicalTag = technicalTag; + } + + private void validateNotNull(final Supporter supporter, final TechnicalTag technicalTag) { + if (Objects.isNull(supporter)) { + throw new SupporterTechnicalTagDomainException("SupporterTechnicalTag 의 supporter 는 null 일 수 없습니다."); + } + + if (Objects.isNull(technicalTag)) { + throw new SupporterTechnicalTagDomainException("SupporterTechnicalTag 의 technicalTag 는 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/technicaltag/command/SupporterTechnicalTags.java b/backend/baton/src/main/java/touch/baton/domain/technicaltag/command/SupporterTechnicalTags.java new file mode 100644 index 000000000..f0ee7b598 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/technicaltag/command/SupporterTechnicalTags.java @@ -0,0 +1,29 @@ +package touch.baton.domain.technicaltag.command; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.OneToMany; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +import static jakarta.persistence.CascadeType.PERSIST; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class SupporterTechnicalTags { + + @OneToMany(mappedBy = "supporter", cascade = PERSIST, orphanRemoval = true) + private List supporterTechnicalTags = new ArrayList<>(); + + public SupporterTechnicalTags(final List supporterTechnicalTags) { + this.supporterTechnicalTags = supporterTechnicalTags; + } + + public void addAll(final List supporterTechnicalTags) { + this.supporterTechnicalTags.addAll(supporterTechnicalTags); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/technicaltag/command/TechnicalTag.java b/backend/baton/src/main/java/touch/baton/domain/technicaltag/command/TechnicalTag.java new file mode 100644 index 000000000..a7b10f2af --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/technicaltag/command/TechnicalTag.java @@ -0,0 +1,46 @@ +package touch.baton.domain.technicaltag.command; + +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.tag.exception.TechnicalTagDomainException; + +import java.util.Objects; + +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class TechnicalTag { + + @GeneratedValue(strategy = IDENTITY) + @Id + private Long id; + + @Embedded + private TagName tagName; + + @Builder + private TechnicalTag(final TagName tagName) { + this(null, tagName); + } + + private TechnicalTag(final Long id, final TagName tagName) { + validateNotNull(tagName); + this.id = id; + this.tagName = tagName; + } + + private void validateNotNull(final TagName tagName) { + if (Objects.isNull(tagName)) { + throw new TechnicalTagDomainException("TechnicalTag 의 tagName 은 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/technicaltag/command/repository/RunnerTechnicalTagCommandRepository.java b/backend/baton/src/main/java/touch/baton/domain/technicaltag/command/repository/RunnerTechnicalTagCommandRepository.java new file mode 100644 index 000000000..e50420098 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/technicaltag/command/repository/RunnerTechnicalTagCommandRepository.java @@ -0,0 +1,15 @@ +package touch.baton.domain.technicaltag.command.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 touch.baton.domain.member.command.Runner; +import touch.baton.domain.technicaltag.command.RunnerTechnicalTag; + +public interface RunnerTechnicalTagCommandRepository extends JpaRepository { + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("DELETE FROM RunnerTechnicalTag rt WHERE rt.runner = :runner") + int deleteByRunner(@Param("runner") final Runner runner); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/technicaltag/command/repository/SupporterTechnicalTagCommandRepository.java b/backend/baton/src/main/java/touch/baton/domain/technicaltag/command/repository/SupporterTechnicalTagCommandRepository.java new file mode 100644 index 000000000..325415b66 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/technicaltag/command/repository/SupporterTechnicalTagCommandRepository.java @@ -0,0 +1,15 @@ +package touch.baton.domain.technicaltag.command.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 touch.baton.domain.member.command.Supporter; +import touch.baton.domain.technicaltag.command.SupporterTechnicalTag; + +public interface SupporterTechnicalTagCommandRepository extends JpaRepository { + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("DELETE FROM SupporterTechnicalTag st WHERE st.supporter = :supporter") + int deleteBySupporter(@Param("supporter") final Supporter supporter); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/technicaltag/query/repository/TechnicalTagQueryRepository.java b/backend/baton/src/main/java/touch/baton/domain/technicaltag/query/repository/TechnicalTagQueryRepository.java new file mode 100644 index 000000000..7550fb99e --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/technicaltag/query/repository/TechnicalTagQueryRepository.java @@ -0,0 +1,12 @@ +package touch.baton.domain.technicaltag.query.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.technicaltag.command.TechnicalTag; + +import java.util.Optional; + +public interface TechnicalTagQueryRepository extends JpaRepository { + + Optional findByTagName(final TagName tagName); +} 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 ab1870e8a..01076ede6 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 @@ -12,8 +12,8 @@ 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; +import touch.baton.domain.oauth.command.AuthorizationHeader; +import touch.baton.domain.oauth.command.exception.OauthRequestException; @Profile("!test") @RequiredArgsConstructor diff --git a/backend/baton/src/main/java/touch/baton/infra/auth/oauth/github/authcode/GithubAuthCodeRequestUrlProvider.java b/backend/baton/src/main/java/touch/baton/infra/auth/oauth/github/authcode/GithubAuthCodeRequestUrlProvider.java index 1fed7b572..b443bca4b 100644 --- a/backend/baton/src/main/java/touch/baton/infra/auth/oauth/github/authcode/GithubAuthCodeRequestUrlProvider.java +++ b/backend/baton/src/main/java/touch/baton/infra/auth/oauth/github/authcode/GithubAuthCodeRequestUrlProvider.java @@ -3,8 +3,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.web.util.UriComponentsBuilder; -import touch.baton.domain.oauth.OauthType; -import touch.baton.domain.oauth.authcode.AuthCodeRequestUrlProvider; +import touch.baton.domain.oauth.command.OauthType; +import touch.baton.domain.oauth.command.authcode.AuthCodeRequestUrlProvider; import touch.baton.infra.auth.oauth.github.GithubOauthConfig; @RequiredArgsConstructor diff --git a/backend/baton/src/main/java/touch/baton/infra/auth/oauth/github/client/GithubInformationClient.java b/backend/baton/src/main/java/touch/baton/infra/auth/oauth/github/client/GithubInformationClient.java index 664d90d21..4e9f47a71 100644 --- a/backend/baton/src/main/java/touch/baton/infra/auth/oauth/github/client/GithubInformationClient.java +++ b/backend/baton/src/main/java/touch/baton/infra/auth/oauth/github/client/GithubInformationClient.java @@ -2,9 +2,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import touch.baton.domain.oauth.OauthInformation; -import touch.baton.domain.oauth.OauthType; -import touch.baton.domain.oauth.client.OauthInformationClient; +import touch.baton.domain.oauth.command.OauthInformation; +import touch.baton.domain.oauth.command.OauthType; +import touch.baton.domain.oauth.command.client.OauthInformationClient; import touch.baton.infra.auth.oauth.github.GithubOauthConfig; import touch.baton.infra.auth.oauth.github.http.GithubHttpInterface; import touch.baton.infra.auth.oauth.github.request.GithubTokenRequest; 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 05e5001c7..739411625 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 @@ -1,19 +1,20 @@ package touch.baton.infra.auth.oauth.github.response; import com.fasterxml.jackson.databind.annotation.JsonNaming; -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.OauthInformation; -import touch.baton.domain.oauth.token.SocialToken; +import jakarta.annotation.Nullable; +import touch.baton.domain.member.command.vo.GithubUrl; +import touch.baton.domain.member.command.vo.ImageUrl; +import touch.baton.domain.member.command.vo.MemberName; +import touch.baton.domain.member.command.vo.OauthId; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.oauth.command.OauthInformation; +import touch.baton.domain.oauth.command.token.SocialToken; import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; @JsonNaming(SnakeCaseStrategy.class) public record GithubMemberResponse(String id, - String name, + @Nullable String name, String login, String htmlUrl, String avatarUrl 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 index 8d86a1066..9b894fe93 100644 --- a/backend/baton/src/main/java/touch/baton/infra/github/GithubBranchManager.java +++ b/backend/baton/src/main/java/touch/baton/infra/github/GithubBranchManager.java @@ -13,7 +13,7 @@ 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.domain.member.command.service.GithubBranchManageable; import touch.baton.infra.exception.InfraException; import touch.baton.infra.github.request.CreateBranchRequest; import touch.baton.infra.github.response.ReadBranchInfoResponse; diff --git a/backend/baton/src/main/resources/application.yml b/backend/baton/src/main/resources/application.yml index ddb45ab4b..c99aff939 100644 --- a/backend/baton/src/main/resources/application.yml +++ b/backend/baton/src/main/resources/application.yml @@ -5,6 +5,7 @@ spring: jpa: properties: hibernate: + default_batch_fetch_size: 50 format_sql: true hibernate: ddl-auto: validate diff --git a/backend/baton/src/main/resources/db/migration/V20231007_1__create_table_notification.sql b/backend/baton/src/main/resources/db/migration/V20231007_1__create_table_notification.sql new file mode 100644 index 000000000..1015c6727 --- /dev/null +++ b/backend/baton/src/main/resources/db/migration/V20231007_1__create_table_notification.sql @@ -0,0 +1,13 @@ +CREATE TABLE notification +( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + message VARCHAR(255) NOT NULL, + notification_type VARCHAR(255) NOT NULL, + is_read bit default false NOT NULL, + referenced_id BIGINT NOT NULL, + member_id BIGINT NOT NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + deleted_at DATETIME(6) +); diff --git a/backend/baton/src/main/resources/db/migration/V20231007_2__alter_table_notification_constraint_fk.sql b/backend/baton/src/main/resources/db/migration/V20231007_2__alter_table_notification_constraint_fk.sql new file mode 100644 index 000000000..eb7cf8c22 --- /dev/null +++ b/backend/baton/src/main/resources/db/migration/V20231007_2__alter_table_notification_constraint_fk.sql @@ -0,0 +1,3 @@ +ALTER TABLE notification + ADD CONSTRAINT fk_notification_to_member + FOREIGN KEY (member_id) REFERENCES member (id); diff --git a/backend/baton/src/main/resources/db/migration/V20231011__create_review_status_index_on_runner_post.sql b/backend/baton/src/main/resources/db/migration/V20231011__create_review_status_index_on_runner_post.sql new file mode 100644 index 000000000..05fbc5f41 --- /dev/null +++ b/backend/baton/src/main/resources/db/migration/V20231011__create_review_status_index_on_runner_post.sql @@ -0,0 +1,2 @@ +create index idx_runner_post_review_status on runner_post (review_status); + 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 ef53e2a55..edcfe5ec3 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 @@ -144,6 +144,25 @@ public static ExtractableResponse get(final String uri, final String a .extract(); } + public static ExtractableResponse patch(final String uri) { + return RestAssured + .given().log().ifValidationFails() + .when().log().ifValidationFails() + .patch(uri) + .then().log().ifError() + .extract(); + } + + public static ExtractableResponse patch(final String uri, final String accessToken) { + return RestAssured + .given().log().ifValidationFails() + .auth().preemptive().oauth2(accessToken) + .when().log().ifValidationFails() + .patch(uri) + .then().log().ifError() + .extract(); + } + public static ExtractableResponse patch(final String uri, final String accessToken, final PathParams pathParams) { return RestAssured .given().log().ifValidationFails() 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 index 906c71dfa..ef620da04 100644 --- a/backend/baton/src/test/java/touch/baton/assure/common/JwtTestManager.java +++ b/backend/baton/src/test/java/touch/baton/assure/common/JwtTestManager.java @@ -3,7 +3,7 @@ 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.domain.member.command.vo.SocialId; import touch.baton.infra.auth.jwt.JwtDecoder; import static touch.baton.fixture.vo.AuthorizationHeaderFixture.bearerAuthorizationHeader; 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 index 2c6309136..c4d2a627a 100644 --- a/backend/baton/src/test/java/touch/baton/assure/common/OauthLoginTestManager.java +++ b/backend/baton/src/test/java/touch/baton/assure/common/OauthLoginTestManager.java @@ -1,8 +1,9 @@ package touch.baton.assure.common; import touch.baton.assure.oauth.OauthAssuredSupport; -import touch.baton.domain.oauth.OauthType; +import touch.baton.domain.oauth.command.OauthType; +@SuppressWarnings("NonAsciiCharacters") public class OauthLoginTestManager { public String 소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(final String 테스트용_사용자_MockAuthCode) { diff --git a/backend/baton/src/test/java/touch/baton/assure/feedback/command/SupporterFeedbackCreateAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/feedback/command/SupporterFeedbackCreateAssuredTest.java new file mode 100644 index 000000000..44a383f87 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/feedback/command/SupporterFeedbackCreateAssuredTest.java @@ -0,0 +1,103 @@ +package touch.baton.assure.feedback.command; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import touch.baton.assure.common.HttpStatusAndLocationHeader; +import touch.baton.assure.feedback.support.command.SupporterFeedbackCreateAssuredSupport; +import touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport; +import touch.baton.assure.runnerpost.support.command.RunnerPostUpdateSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.vo.SocialId; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.springframework.http.HttpStatus.CREATED; +import static touch.baton.assure.feedback.support.command.SupporterFeedbackCreateAssuredSupport.서포터_피드백_요청; +import static touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport.러너_게시글_생성_요청; +import static touch.baton.assure.runnerpost.support.command.applicant.RunnerPostApplicantCreateSupport.러너의_서포터_선택_요청; +import static touch.baton.assure.runnerpost.support.command.applicant.RunnerPostApplicantCreateSupport.클라이언트_요청; + +@SuppressWarnings("NonAsciiCharacters") +class SupporterFeedbackCreateAssuredTest extends AssuredTestConfig { + + @Test + void 러너가_서포터_피드백을_등록한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Supporter 서포터_헤나 = supporterRepository.getBySocialId(헤나_소셜_아이디); + + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + 러너가_서포터의_리뷰_신청_선택에_성공한다(서포터_헤나, 디투_액세스_토큰, 디투_러너_게시글_식별자값); + 서포터가_러너_게시글의_리뷰를_완료로_변경하는_것을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + + // then + SupporterFeedbackCreateAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(디투_액세스_토큰) + .서포터_피드백을_등록한다( + 서포터_피드백_요청("GOOD", List.of("코드리뷰가 맛있어요.", "말투가 친절해요."), 서포터_헤나.getId(), 디투_러너_게시글_식별자값) + ) + + .서버_응답() + .서포터_피드백_등록_성공을_검증한다(new HttpStatusAndLocationHeader(CREATED, "/api/v1/feedback/supporter")); + } + + private Long 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(final String 헤나_액세스_토큰) { + return RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다(러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + List.of("자바", "스프링"), + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + ) + ) + + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); + } + + private void 서포터가_러너_게시글에_리뷰_신청을_성공한다(final String 헤나_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + 클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .서포터가_러너_게시글에_리뷰를_신청한다(디투_러너_게시글_식별자값, "안녕하세요. 서포터 헤나입니다.") + + .서버_응답() + .서포터가_러너_게시글에_리뷰_신청_성공을_검증한다(디투_러너_게시글_식별자값); + } + + private void 러너가_서포터의_리뷰_신청_선택에_성공한다(final Supporter 서포터_헤나, final String 디투_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + RunnerPostUpdateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(디투_액세스_토큰) + .러너가_서포터를_선택한다(디투_러너_게시글_식별자값, 러너의_서포터_선택_요청(서포터_헤나.getId())) + + .서버_응답() + .러너_게시글에_서포터가_성공적으로_선택되었는지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); + } + + private void 서포터가_러너_게시글의_리뷰를_완료로_변경하는_것을_성공한다(final String 헤나_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + RunnerPostUpdateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .서포터가_리뷰를_완료하고_리뷰완료_버튼을_누른다(디투_러너_게시글_식별자값) + + .서버_응답() + .러너_게시글이_성공적으로_리뷰_완료_상태인지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); + } + + // FIXME: 2023/09/19 피드백 실패 테스트 추가해줘잉 +} diff --git a/backend/baton/src/test/java/touch/baton/assure/feedback/support/command/SupporterFeedbackCreateAssuredSupport.java b/backend/baton/src/test/java/touch/baton/assure/feedback/support/command/SupporterFeedbackCreateAssuredSupport.java new file mode 100644 index 000000000..311dc83ff --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/feedback/support/command/SupporterFeedbackCreateAssuredSupport.java @@ -0,0 +1,68 @@ +package touch.baton.assure.feedback.support.command; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.springframework.http.HttpHeaders; +import touch.baton.assure.common.AssuredSupport; +import touch.baton.assure.common.HttpStatusAndLocationHeader; +import touch.baton.domain.feedback.command.service.dto.SupporterFeedBackCreateRequest; + +import java.util.List; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +public class SupporterFeedbackCreateAssuredSupport { + + private SupporterFeedbackCreateAssuredSupport() { + } + + public static SupporterFeedbackCreateBuilder 클라이언트_요청() { + return new SupporterFeedbackCreateBuilder(); + } + + public static SupporterFeedBackCreateRequest 서포터_피드백_요청(final String 리뷰_타입, + final List 디스크립션, + final Long 서포터_식별자값, + final Long 러너_게시글_식별자값 + ) { + return new SupporterFeedBackCreateRequest(리뷰_타입, 디스크립션, 서포터_식별자값, 러너_게시글_식별자값); + } + + public static class SupporterFeedbackCreateBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public SupporterFeedbackCreateBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; + return this; + } + + public SupporterFeedbackCreateBuilder 서포터_피드백을_등록한다(final SupporterFeedBackCreateRequest 서포터_피드백_정보) { + response = AssuredSupport.post("/api/v1/feedback/supporter", accessToken, 서포터_피드백_정보); + return this; + } + + public SupporterFeedbackCreateResponseBuilder 서버_응답() { + return new SupporterFeedbackCreateResponseBuilder(response); + } + } + + public static class SupporterFeedbackCreateResponseBuilder { + + private final ExtractableResponse response; + + public SupporterFeedbackCreateResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + + public void 서포터_피드백_등록_성공을_검증한다(final HttpStatusAndLocationHeader 응답상태_및_로케이션) { + assertSoftly(softly -> { + 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/member/command/MemberBranchAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/member/command/MemberBranchAssuredTest.java new file mode 100644 index 000000000..bea28873d --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/member/command/MemberBranchAssuredTest.java @@ -0,0 +1,28 @@ +package touch.baton.assure.member.command; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.common.HttpStatusAndLocationHeader; +import touch.baton.assure.member.support.command.MemberBranchCreateAssuredSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.member.command.service.dto.GithubRepoNameRequest; + +import static org.springframework.http.HttpStatus.CREATED; + +@SuppressWarnings("NonAsciiCharacters") +class MemberBranchAssuredTest extends AssuredTestConfig { + + @Test + void 로그인_한_사용자가_요청한_레포의_브랜치를_생성한다() { + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode()); + final GithubRepoNameRequest 브랜치_생성_요청 = new GithubRepoNameRequest("ditoo"); + + MemberBranchCreateAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인_한다(디투_액세스_토큰) + .입력받은_레포에_사용자_github_계정명으로_된_브랜치를_생성한다(브랜치_생성_요청) + + .서버_응답() + .레포에_브랜치_등록_성공을_검증한다(new HttpStatusAndLocationHeader(CREATED, "/api/v1/profile/me")); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/member/command/RunnerUpdateAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/member/command/RunnerUpdateAssuredTest.java new file mode 100644 index 000000000..63d351b8d --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/member/command/RunnerUpdateAssuredTest.java @@ -0,0 +1,131 @@ +package touch.baton.assure.member.command; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.common.HttpStatusAndLocationHeader; +import touch.baton.assure.member.support.command.RunnerUpdateAssuredSupport; +import touch.baton.assure.member.support.query.RunnerQueryAssuredSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.service.dto.RunnerUpdateRequest; +import touch.baton.domain.member.command.vo.SocialId; + +import java.util.List; + +import static org.springframework.http.HttpStatus.NO_CONTENT; +import static touch.baton.assure.member.support.command.RunnerUpdateAssuredSupport.러너_본인_프로필_수정_요청; +import static touch.baton.assure.member.support.query.RunnerQueryAssuredSupport.RunnerQueryResponseBuilder.러너_프로필_상세_응답; +import static touch.baton.domain.common.exception.ClientErrorCode.*; + +@SuppressWarnings("NonAsciiCharacters") +class RunnerUpdateAssuredTest extends AssuredTestConfig { + + @Test + void 러너_정보를_수정한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final RunnerUpdateRequest 러너_본인_프로필_수정_요청 = RunnerUpdateAssuredSupport.러너_본인_프로필_수정_요청("수정된_헤나", "수정된_회사", "수정된_러너_소개글", List.of("자바", "스프링")); + + // when, then + RunnerUpdateAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_본인_프로필을_수정한다(러너_본인_프로필_수정_요청) + + .서버_응답() + .러너_본인_프로필_수정_성공을_검증한다(new HttpStatusAndLocationHeader(NO_CONTENT, "/api/v1/profile/runner/me")); + } + + @Test + void 러너_정보_수정_시에_이름이_없으면_예외가_발생한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final RunnerUpdateRequest 러너_본인_프로필_수정_요청 = RunnerUpdateAssuredSupport.러너_본인_프로필_수정_요청(null, "수정된_회사", "수정된_러너_소개글", List.of("자바", "스프링")); + + // when, then + RunnerUpdateAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_본인_프로필을_수정한다(러너_본인_프로필_수정_요청) + + .서버_응답() + .러너_본인_프로필_수정_실패를_검증한다(NAME_IS_NULL); + } + + @Test + void 서포터_정보_수정_시에_소속이_없으면_예외가_발생한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + // when, then + final RunnerUpdateRequest 러너_본인_프로필_수정_요청 = RunnerUpdateAssuredSupport.러너_본인_프로필_수정_요청("수정된_헤나", null, "수정된_러너_소개글", List.of("자바", "스프링")); + RunnerUpdateAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_본인_프로필을_수정한다(러너_본인_프로필_수정_요청) + + .서버_응답() + .러너_본인_프로필_수정_실패를_검증한다(COMPANY_IS_NULL); + } + + @Test + void 러너_정보_수정_시에_소개글이_없으면_예외가_발생한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + // when, then + final RunnerUpdateRequest 러너_본인_프로필_수정_요청 = RunnerUpdateAssuredSupport.러너_본인_프로필_수정_요청("수정된_헤나", "수정된_회사", null, List.of("자바", "스프링")); + RunnerUpdateAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_본인_프로필을_수정한다(러너_본인_프로필_수정_요청) + + .서버_응답() + .러너_본인_프로필_수정_실패를_검증한다(RUNNER_INTRODUCTION_IS_NULL); + } + + @Test + void 러너_정보_수정_시에_기술_태그가_없으면_예외가_발생한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + // when, then + final RunnerUpdateRequest 러너_본인_프로필_수정_요청 = RunnerUpdateAssuredSupport.러너_본인_프로필_수정_요청("수정된_헤나", "수정된_회사", "수정된_러너_소개글", null); + RunnerUpdateAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_본인_프로필을_수정한다(러너_본인_프로필_수정_요청) + + .서버_응답() + .러너_본인_프로필_수정_실패를_검증한다(RUNNER_TECHNICAL_TAGS_ARE_NULL); + } + + @Test + void 수정된_러너_프로필_조회에_성공한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Runner 러너_헤나 = runnerRepository.getBySocialId(헤나_소셜_아이디); + + final RunnerUpdateRequest 러너_본인_프로필_수정_요청 = 러너_본인_프로필_수정_요청("수정된_헤나", "수정된_회사", "수정된_러너_소개글", List.of("자바", "스프링")); + RunnerUpdateAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_본인_프로필을_수정한다(러너_본인_프로필_수정_요청) + + .서버_응답() + .러너_본인_프로필_수정_성공을_검증한다(new HttpStatusAndLocationHeader(NO_CONTENT, "/api/v1/profile/runner/me")); + + // when, then + RunnerQueryAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_프로필을_상세_조회한다(러너_헤나.getId()) + + .서버_응답() + .러너_프로필_상세_조회를_검증한다(러너_프로필_상세_응답(러너_헤나, 러너_본인_프로필_수정_요청)); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/member/command/SupporterUpdateAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/member/command/SupporterUpdateAssuredTest.java new file mode 100644 index 000000000..ae9e393cd --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/member/command/SupporterUpdateAssuredTest.java @@ -0,0 +1,101 @@ +package touch.baton.assure.member.command; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.member.support.command.SupporterUpdateAssuredSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; + +import java.util.List; + +import static org.springframework.http.HttpStatus.NO_CONTENT; +import static touch.baton.assure.member.support.command.SupporterUpdateAssuredSupport.서포터_본인_정보_수정_요청; +import static touch.baton.domain.common.exception.ClientErrorCode.*; + +@SuppressWarnings("NonAsciiCharacters") +class SupporterUpdateAssuredTest extends AssuredTestConfig { + + @Test + void 서포터_정보를_수정한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + // when, then + SupporterUpdateAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인_한다(헤나_액세스_토큰) + .서포터_본인_프로필을_수정한다( + 서포터_본인_정보_수정_요청("수정된_이름", "수정된_회사", "수정된_서포터_자기소개글", List.of("자바", "스프링")) + ) + + .서버_응답() + .서포터_본인_프로필_수정_성공을_검증한다(NO_CONTENT); + } + + @Test + void 서포터_정보_수정_시에_이름이_없으면_예외가_발생한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + // when, then + SupporterUpdateAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인_한다(헤나_액세스_토큰) + .서포터_본인_프로필을_수정한다( + 서포터_본인_정보_수정_요청(null, "수정된_회사", "수정된_서포터_자기소개글", List.of("자바", "스프링")) + ) + + .서버_응답() + .서포터_본인_프로필_수정_실패를_검증한다(NAME_IS_NULL); + } + + @Test + void 서포터_정보_수정_시에_소속이_없으면_예외가_발생한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + // when, then + SupporterUpdateAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인_한다(헤나_액세스_토큰) + .서포터_본인_프로필을_수정한다( + 서포터_본인_정보_수정_요청("수정된_이름", null, "수정된_서포터_자기소개글", List.of("자바", "스프링")) + ) + + .서버_응답() + .서포터_본인_프로필_수정_실패를_검증한다(COMPANY_IS_NULL); + } + + @Test + void 서포터_정보_수정_시에_소개글이_없으면_예외가_발생한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + // when, then + SupporterUpdateAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인_한다(헤나_액세스_토큰) + .서포터_본인_프로필을_수정한다( + 서포터_본인_정보_수정_요청("수정된_이름", "수정된_회사", null, List.of("자바", "스프링")) + ) + + .서버_응답() + .서포터_본인_프로필_수정_실패를_검증한다(SUPPORTER_INTRODUCTION_IS_NULL); + } + + @Test + void 서포터_정보_수정_시에_기술_태그가_없으면_예외가_발생한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + // when, then + SupporterUpdateAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인_한다(헤나_액세스_토큰) + .서포터_본인_프로필을_수정한다( + 서포터_본인_정보_수정_요청("수정된_이름", "수정된_회사", "수정된_서포터_자기소개글", null) + ) + + .서버_응답() + .서포터_본인_프로필_수정_실패를_검증한다(SUPPORTER_TECHNICAL_TAGS_ARE_NULL); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/member/query/MemberQueryAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/member/query/MemberQueryAssuredTest.java new file mode 100644 index 000000000..dfa419f8f --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/member/query/MemberQueryAssuredTest.java @@ -0,0 +1,30 @@ +package touch.baton.assure.member.query; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.member.support.query.MemberQueryAssuredSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.vo.SocialId; + +import static touch.baton.assure.member.support.query.MemberQueryAssuredSupport.로그인한_사용자_프로필_응답; + +@SuppressWarnings("NonAsciiCharacters") +class MemberQueryAssuredTest extends AssuredTestConfig { + + @Test + void 로그인_한_사용자_프로필을_조회한다() { + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Member 사용자_헤나 = memberRepository.getBySocialId(헤나_소셜_아이디); + + MemberQueryAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인_한다(헤나_액세스_토큰) + .사용자_본인_프로필을_가지고_있는_액세스_토큰으로_조회한다() + + .서버_응답() + .로그인한_사용자_프로필_조회_성공을_검증한다(로그인한_사용자_프로필_응답(사용자_헤나)); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/member/query/RunnerQueryAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/member/query/RunnerQueryAssuredTest.java new file mode 100644 index 000000000..f32c1ad02 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/member/query/RunnerQueryAssuredTest.java @@ -0,0 +1,33 @@ +package touch.baton.assure.member.query; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.member.support.command.RunnerUpdateAssuredSupport; +import touch.baton.assure.member.support.query.RunnerQueryAssuredSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.vo.SocialId; + +import java.util.Collections; + +@SuppressWarnings("NonAsciiCharacters") +class RunnerQueryAssuredTest extends AssuredTestConfig { + + @Test + void 러너_본인_프로필을_가지고_있는_액세스_토큰으로_조회에_성공한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Runner 러너_헤나 = runnerRepository.getBySocialId(헤나_소셜_아이디); + + // when, then + RunnerQueryAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_본인_프로필을_가지고_있는_액세스_토큰으로_조회한다() + + .서버_응답() + .러너_본인_프로필_조회_성공을_검증한다(RunnerUpdateAssuredSupport.러너_본인_프로필_응답(러너_헤나, Collections.emptyList())); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/member/query/SupporterQueryAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/member/query/SupporterQueryAssuredTest.java new file mode 100644 index 000000000..d8018e307 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/member/query/SupporterQueryAssuredTest.java @@ -0,0 +1,52 @@ +package touch.baton.assure.member.query; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.member.support.query.SupporterQueryAssuredSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.vo.SocialId; + +import java.util.Collections; + +import static touch.baton.assure.member.support.query.SupporterQueryAssuredSupport.서포터_MyProfile_응답; +import static touch.baton.assure.member.support.query.SupporterQueryAssuredSupport.서포터_Profile_응답; + +@SuppressWarnings("NonAsciiCharacters") +class SupporterQueryAssuredTest extends AssuredTestConfig { + + @Test + void 서포터_프로필을_조회한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Supporter 서포터_헤나 = supporterRepository.getBySocialId(헤나_소셜_아이디); + + // when, then + SupporterQueryAssuredSupport + .클라이언트_요청() + .서포터_프로필을_서포터_식별자값으로_조회한다(서포터_헤나.getId()) + + .서버_응답() + .서포터_프로필_조회_성공을_검증한다(서포터_Profile_응답(서포터_헤나, Collections.emptyList())); + } + + @Test + void 서포터_마이페이지_프로필을_조회한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Supporter 서포터_헤나 = supporterRepository.getBySocialId(헤나_소셜_아이디); + + // when, then + SupporterQueryAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인_한다(헤나_액세스_토큰) + .서포터_마이페이지를_액세스_토큰으로_조회한다() + + .서버_응답() + .서포터_마이페이지_프로필_조회_성공을_검증한다(서포터_MyProfile_응답(서포터_헤나, Collections.emptyList())); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/member/support/command/MemberBranchCreateAssuredSupport.java b/backend/baton/src/test/java/touch/baton/assure/member/support/command/MemberBranchCreateAssuredSupport.java new file mode 100644 index 000000000..916bbf729 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/member/support/command/MemberBranchCreateAssuredSupport.java @@ -0,0 +1,59 @@ +package touch.baton.assure.member.support.command; + +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.command.service.dto.GithubRepoNameRequest; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.springframework.http.HttpHeaders.LOCATION; + +@SuppressWarnings("NonAsciiCharacters") +public class MemberBranchCreateAssuredSupport { + + private MemberBranchCreateAssuredSupport() { + } + + public static MemberBranchCreateBuilder 클라이언트_요청() { + return new MemberBranchCreateBuilder(); + } + + public static class MemberBranchCreateBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public MemberBranchCreateBuilder 액세스_토큰으로_로그인_한다(final String 액세스_토큰) { + accessToken = 액세스_토큰; + return this; + } + + public MemberBranchCreateBuilder 입력받은_레포에_사용자_github_계정명으로_된_브랜치를_생성한다(final GithubRepoNameRequest 레포_이름_요청) { + response = AssuredSupport.post("/api/v1/branch", accessToken, 레포_이름_요청); + return this; + } + + public MemberBranchCreateResponseBuilder 서버_응답() { + return new MemberBranchCreateResponseBuilder(response); + } + } + + public static class MemberBranchCreateResponseBuilder { + + private final ExtractableResponse response; + + public MemberBranchCreateResponseBuilder(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/support/command/RunnerUpdateAssuredSupport.java b/backend/baton/src/test/java/touch/baton/assure/member/support/command/RunnerUpdateAssuredSupport.java new file mode 100644 index 000000000..25c24a3cd --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/member/support/command/RunnerUpdateAssuredSupport.java @@ -0,0 +1,96 @@ +package touch.baton.assure.member.support.command; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.springframework.http.HttpHeaders; +import touch.baton.assure.common.AssuredSupport; +import touch.baton.assure.common.HttpStatusAndLocationHeader; +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.response.ErrorResponse; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.service.dto.RunnerUpdateRequest; +import touch.baton.domain.member.query.controller.response.RunnerResponse; + +import java.util.List; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +public class RunnerUpdateAssuredSupport { + + private RunnerUpdateAssuredSupport() { + } + + public static RunnerUpdateBuilder 클라이언트_요청() { + return new RunnerUpdateBuilder(); + } + + public static RunnerResponse.Mine 러너_본인_프로필_응답(final Runner 러너, final List 러너_태그_목록) { + return new RunnerResponse.Mine( + 러너.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 class RunnerUpdateBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public RunnerUpdateBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; + return this; + } + + public RunnerUpdateBuilder 러너_본인_프로필을_수정한다(final RunnerUpdateRequest 러너_업데이트_요청) { + response = AssuredSupport.patch("/api/v1/profile/runner/me", accessToken, 러너_업데이트_요청); + return this; + } + + public RunnerUpdateResponseBuilder 서버_응답() { + return new RunnerUpdateResponseBuilder(response); + } + } + + public static class RunnerUpdateResponseBuilder { + + private final ExtractableResponse response; + + public RunnerUpdateResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + + public static RunnerUpdateBuilder 클라이언트_요청() { + return new RunnerUpdateBuilder(); + } + + public void 러너_본인_프로필_수정_성공을_검증한다(final HttpStatusAndLocationHeader 응답상태_및_로케이션) { + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(응답상태_및_로케이션.getHttpStatus().value()); + softly.assertThat(response.header(HttpHeaders.LOCATION)).contains(응답상태_및_로케이션.getLocation()); + }); + } + + public void 러너_본인_프로필_수정_실패를_검증한다(final ClientErrorCode 클라이언트_에러_코드) { + final ErrorResponse actual = this.response.as(ErrorResponse.class); + + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(클라이언트_에러_코드.getHttpStatus().value()); + softly.assertThat(actual.errorCode()).isEqualTo(클라이언트_에러_코드.getErrorCode()); + }); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/member/support/command/SupporterUpdateAssuredSupport.java b/backend/baton/src/test/java/touch/baton/assure/member/support/command/SupporterUpdateAssuredSupport.java new file mode 100644 index 000000000..26aa67ee0 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/member/support/command/SupporterUpdateAssuredSupport.java @@ -0,0 +1,78 @@ +package touch.baton.assure.member.support.command; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.springframework.http.HttpStatus; +import touch.baton.assure.common.AssuredSupport; +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.response.ErrorResponse; +import touch.baton.domain.member.command.service.dto.SupporterUpdateRequest; + +import java.util.List; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +public class SupporterUpdateAssuredSupport { + + private SupporterUpdateAssuredSupport() { + } + + public static SupporterUpdateBuilder 클라이언트_요청() { + return new SupporterUpdateBuilder(); + } + + public static SupporterUpdateRequest 서포터_본인_정보_수정_요청(final String 이름, + final String 회사, + final String 서포터_자기소개글, + final List 서포터_기술_태그_목록 + ) { + return new SupporterUpdateRequest(이름, 회사, 서포터_자기소개글, 서포터_기술_태그_목록); + } + + public static class SupporterUpdateBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public SupporterUpdateBuilder 액세스_토큰으로_로그인_한다(final String 액세스_토큰) { + accessToken = 액세스_토큰; + return this; + } + + public SupporterUpdateBuilder 서포터_본인_프로필을_수정한다(final SupporterUpdateRequest 서포터_업데이트_요청) { + response = AssuredSupport.patch("/api/v1/profile/supporter/me", accessToken, 서포터_업데이트_요청); + return this; + } + + public SupporterUpdateResponseBuilder 서버_응답() { + return new SupporterUpdateResponseBuilder(response); + } + } + + public static class SupporterUpdateResponseBuilder { + + private final ExtractableResponse response; + + public SupporterUpdateResponseBuilder(final ExtractableResponse 응답) { + this.response = 응답; + } + + public void 서포터_본인_프로필_수정_성공을_검증한다(final HttpStatus HTTP_STATUS) { + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(HTTP_STATUS.value()); + softly.assertThat(response.header("Location")).isNotNull(); + }); + } + + public void 서포터_본인_프로필_수정_실패를_검증한다(final ClientErrorCode 클라이언트_에러_코드) { + final ErrorResponse actual = this.response.as(ErrorResponse.class); + + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(클라이언트_에러_코드.getHttpStatus().value()); + softly.assertThat(actual.errorCode()).isEqualTo(클라이언트_에러_코드.getErrorCode()); + }); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/member/support/query/MemberQueryAssuredSupport.java b/backend/baton/src/test/java/touch/baton/assure/member/support/query/MemberQueryAssuredSupport.java new file mode 100644 index 000000000..0303f85b5 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/member/support/query/MemberQueryAssuredSupport.java @@ -0,0 +1,63 @@ +package touch.baton.assure.member.support.query; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import touch.baton.assure.common.AssuredSupport; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.controller.response.LoginMemberInfoResponse; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +public class MemberQueryAssuredSupport { + + private MemberQueryAssuredSupport() { + } + + public static MemberQueryBuilder 클라이언트_요청() { + return new MemberQueryBuilder(); + } + + public static LoginMemberInfoResponse 로그인한_사용자_프로필_응답(final Member 맴버) { + return LoginMemberInfoResponse.from(맴버); + } + + public static class MemberQueryBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public MemberQueryBuilder 액세스_토큰으로_로그인_한다(final String 액세스_토큰) { + accessToken = 액세스_토큰; + return this; + } + + public MemberQueryBuilder 사용자_본인_프로필을_가지고_있는_액세스_토큰으로_조회한다() { + response = AssuredSupport.get("api/v1/profile/me", accessToken); + return this; + } + + public MemberQueryResponseBuilder 서버_응답() { + return new MemberQueryResponseBuilder(response); + } + } + + public static class MemberQueryResponseBuilder { + + private final ExtractableResponse response; + + public MemberQueryResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + + public void 로그인한_사용자_프로필_조회_성공을_검증한다(final LoginMemberInfoResponse 맴버_로그인_프로필_응답) { + final LoginMemberInfoResponse actual = this.response.as(LoginMemberInfoResponse.class); + + assertSoftly(softly -> { + softly.assertThat(actual.name()).isEqualTo(맴버_로그인_프로필_응답.name()); + softly.assertThat(actual.imageUrl()).isEqualTo(맴버_로그인_프로필_응답.imageUrl()); + }); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/member/support/query/RunnerQueryAssuredSupport.java b/backend/baton/src/test/java/touch/baton/assure/member/support/query/RunnerQueryAssuredSupport.java new file mode 100644 index 000000000..ec9fecf84 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/member/support/query/RunnerQueryAssuredSupport.java @@ -0,0 +1,103 @@ +package touch.baton.assure.member.support.query; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import touch.baton.assure.common.AssuredSupport; +import touch.baton.assure.common.PathParams; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.service.dto.RunnerUpdateRequest; +import touch.baton.domain.member.query.controller.response.RunnerResponse; + +import java.util.Map; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +public class RunnerQueryAssuredSupport { + + private RunnerQueryAssuredSupport() { + } + + public static RunnerQueryBuilder 클라이언트_요청() { + return new RunnerQueryBuilder(); + } + + public static class RunnerQueryBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public RunnerQueryBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; + return this; + } + + public RunnerQueryBuilder 러너_본인_프로필을_가지고_있는_액세스_토큰으로_조회한다() { + response = AssuredSupport.get("/api/v1/profile/runner/me", accessToken); + return this; + } + + public RunnerQueryBuilder 러너_프로필을_상세_조회한다(final Long 러너_식별자) { + response = AssuredSupport.get("/api/v1/profile/runner/{runnerId}", new PathParams(Map.of("runnerId", 러너_식별자))); + return this; + } + + public RunnerQueryResponseBuilder 서버_응답() { + return new RunnerQueryResponseBuilder(response); + } + } + + public static class RunnerQueryResponseBuilder { + + private final ExtractableResponse response; + + public RunnerQueryResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + + public static RunnerQueryBuilder 클라이언트_요청() { + return new RunnerQueryBuilder(); + } + + public static RunnerResponse.Detail 러너_프로필_상세_응답(final Runner 러너, final RunnerUpdateRequest 러너_본인_프로필_수정_요청) { + return new RunnerResponse.Detail( + 러너.getId(), + 러너_본인_프로필_수정_요청.name(), + 러너.getMember().getImageUrl().getValue(), + 러너.getMember().getGithubUrl().getValue(), + 러너_본인_프로필_수정_요청.introduction(), + 러너_본인_프로필_수정_요청.company(), + 러너_본인_프로필_수정_요청.technicalTags() + ); + } + + public void 러너_본인_프로필_조회_성공을_검증한다(final RunnerResponse.Mine 러너_본인_프로필_응답) { + final RunnerResponse.Mine actual = this.response.as(RunnerResponse.Mine.class); + + assertSoftly(softly -> { + softly.assertThat(actual.name()).isEqualTo(러너_본인_프로필_응답.name()); + softly.assertThat(actual.company()).isEqualTo(러너_본인_프로필_응답.company()); + softly.assertThat(actual.imageUrl()).isEqualTo(러너_본인_프로필_응답.imageUrl()); + softly.assertThat(actual.githubUrl()).isEqualTo(러너_본인_프로필_응답.githubUrl()); + softly.assertThat(actual.introduction()).isEqualTo(러너_본인_프로필_응답.introduction()); + softly.assertThat(actual.technicalTags()).isEqualTo(러너_본인_프로필_응답.technicalTags()); + }); + } + + public void 러너_프로필_상세_조회를_검증한다(final RunnerResponse.Detail 러너_프로필_상세_응답) { + final RunnerResponse.Detail actual = this.response.as(RunnerResponse.Detail.class); + + assertSoftly(softly -> { + softly.assertThat(actual.runnerId()).isNotNull(); + softly.assertThat(actual.name()).isEqualTo(러너_프로필_상세_응답.name()); + softly.assertThat(actual.imageUrl()).isEqualTo(러너_프로필_상세_응답.imageUrl()); + softly.assertThat(actual.githubUrl()).isEqualTo(러너_프로필_상세_응답.githubUrl()); + softly.assertThat(actual.introduction()).isEqualTo(러너_프로필_상세_응답.introduction()); + softly.assertThat(actual.company()).isEqualTo(러너_프로필_상세_응답.company()); + softly.assertThat(actual.technicalTags()).containsExactlyElementsOf(러너_프로필_상세_응답.technicalTags()); + } + ); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/member/support/query/SupporterQueryAssuredSupport.java b/backend/baton/src/test/java/touch/baton/assure/member/support/query/SupporterQueryAssuredSupport.java new file mode 100644 index 000000000..7fbe9149b --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/member/support/query/SupporterQueryAssuredSupport.java @@ -0,0 +1,111 @@ +package touch.baton.assure.member.support.query; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import touch.baton.assure.common.AssuredSupport; +import touch.baton.assure.common.PathParams; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.query.controller.response.SupporterResponse; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +public class SupporterQueryAssuredSupport { + + private SupporterQueryAssuredSupport() { + } + + public static SupporterQueryBuilder 클라이언트_요청() { + return new SupporterQueryBuilder(); + } + + 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 SupporterQueryBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public SupporterQueryBuilder 액세스_토큰으로_로그인_한다(final String 액세스_토큰) { + accessToken = 액세스_토큰; + return this; + } + + public SupporterQueryBuilder 서포터_프로필을_서포터_식별자값으로_조회한다(final Long 서포터_식별자값) { + response = AssuredSupport.get("/api/v1/profile/supporter/{supporterId}", new PathParams(Map.of("supporterId", 서포터_식별자값))); + return this; + } + + public SupporterQueryBuilder 서포터_마이페이지를_액세스_토큰으로_조회한다() { + response = AssuredSupport.get("/api/v1/profile/supporter/me", accessToken); + return this; + } + + public SupporterQueryResponseBuilder 서버_응답() { + return new SupporterQueryResponseBuilder(response); + } + } + + public static class SupporterQueryResponseBuilder { + + private final ExtractableResponse response; + + public SupporterQueryResponseBuilder(final ExtractableResponse 응답) { + this.response = 응답; + } + + public void 서포터_프로필_조회_성공을_검증한다(final SupporterResponse.Profile 서포터_프로필_응답) { + final SupporterResponse.Profile actual = this.response.as(SupporterResponse.Profile.class); + + assertSoftly(softly -> { + softly.assertThat(actual.supporterId()).isEqualTo(서포터_프로필_응답.supporterId()); + softly.assertThat(actual.name()).isEqualTo(서포터_프로필_응답.name()); + softly.assertThat(actual.company()).isEqualTo(서포터_프로필_응답.company()); + softly.assertThat(actual.imageUrl()).isEqualTo(서포터_프로필_응답.imageUrl()); + softly.assertThat(actual.githubUrl()).isEqualTo(서포터_프로필_응답.githubUrl()); + softly.assertThat(actual.introduction()).isEqualTo(서포터_프로필_응답.introduction()); + softly.assertThat(actual.technicalTags()).isEqualTo(서포터_프로필_응답.technicalTags()); + } + ); + } + + public void 서포터_마이페이지_프로필_조회_성공을_검증한다(final SupporterResponse.MyProfile 서포터_마이페이지_프로필_응답) { + final SupporterResponse.MyProfile actual = this.response.as(SupporterResponse.MyProfile.class); + + assertSoftly(softly -> { + softly.assertThat(actual.name()).isEqualTo(서포터_마이페이지_프로필_응답.name()); + softly.assertThat(actual.company()).isEqualTo(서포터_마이페이지_프로필_응답.company()); + softly.assertThat(actual.imageUrl()).isEqualTo(서포터_마이페이지_프로필_응답.imageUrl()); + softly.assertThat(actual.githubUrl()).isEqualTo(서포터_마이페이지_프로필_응답.githubUrl()); + softly.assertThat(actual.introduction()).isEqualTo(서포터_마이페이지_프로필_응답.introduction()); + softly.assertThat(actual.technicalTags()).isEqualTo(서포터_마이페이지_프로필_응답.technicalTags()); + } + ); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/notification/command/NotificationDeleteAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/notification/command/NotificationDeleteAssuredTest.java new file mode 100644 index 000000000..76f47dc92 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/notification/command/NotificationDeleteAssuredTest.java @@ -0,0 +1,73 @@ +package touch.baton.assure.notification.command; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.notification.support.command.NotificationDeleteSupport; +import touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.notification.command.Notification; +import touch.baton.domain.notification.command.vo.NotificationMessage; +import touch.baton.domain.notification.command.vo.NotificationReferencedId; +import touch.baton.domain.notification.command.vo.NotificationTitle; +import touch.baton.domain.notification.command.vo.NotificationType; +import touch.baton.domain.notification.command.vo.IsRead; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostCreateRequest; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +class NotificationDeleteAssuredTest extends AssuredTestConfig { + + @Test + void 로그인한_사용자가_자신의_알림을_하나_삭제한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest( + "테스트용 게시글 제목", + List.of("테스트용 태그1", "테스트용 태그2"), + "https://github.com/test", + LocalDateTime.now().plusDays(10), + "테스트용 구현 내용", + "테스트용 궁금한 내용", + "테스트용 남길 내용" + ); + + final Long 생성된_러너_게시글_식별자값 = RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다(게시글_생성_요청) + + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); + + // when & then + final Notification 저장된_알림 = 러너_게시글_작성자에게_알림을_저장한다(생성된_러너_게시글_식별자값); + + NotificationDeleteSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .알림_삭제에_성공한다(저장된_알림.getId()) + + .서버_응답() + .알림_삭제_성공을_검증한다(); + } + + private Notification 러너_게시글_작성자에게_알림을_저장한다(final Long 생성된_러너_게시글_식별자값) { + final Member 헤나_사용자 = memberRepository.getAsRunnerByRunnerPostId(생성된_러너_게시글_식별자값); + + final Notification 저장되지_않은_알림 = Notification.builder() + .notificationTitle(new NotificationTitle("알림 테스트용 제목")) + .notificationMessage(new NotificationMessage("알림 테스트용 내용")) + .notificationType(NotificationType.RUNNER_POST) + .notificationReferencedId(new NotificationReferencedId(1L)) + .isRead(IsRead.asUnRead()) + .member(헤나_사용자) + .build(); + + return notificationCommandRepository.save(저장되지_않은_알림); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/notification/command/NotificationUpdateAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/notification/command/NotificationUpdateAssuredTest.java new file mode 100644 index 000000000..14be54de0 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/notification/command/NotificationUpdateAssuredTest.java @@ -0,0 +1,73 @@ +package touch.baton.assure.notification.command; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.notification.support.command.NotificationUpdateSupport; +import touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.notification.command.Notification; +import touch.baton.domain.notification.command.vo.NotificationMessage; +import touch.baton.domain.notification.command.vo.NotificationReferencedId; +import touch.baton.domain.notification.command.vo.NotificationTitle; +import touch.baton.domain.notification.command.vo.NotificationType; +import touch.baton.domain.notification.command.vo.IsRead; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostCreateRequest; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +class NotificationUpdateAssuredTest extends AssuredTestConfig { + + @Test + void 로그인한_사용자의_알림_읽기_여부_기록을_업데이트한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest( + "테스트용 게시글 제목", + List.of("테스트용 태그1", "테스트용 태그2"), + "https://github.com/test", + LocalDateTime.now().plusDays(10), + "테스트용 구현 내용", + "테스트용 궁금한 내용", + "테스트용 남길 내용" + ); + + final Long 생성된_러너_게시글_식별자값 = RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다(게시글_생성_요청) + + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); + + // when & then + final Notification 저장된_알림 = 러너_게시글_작성자에게_알림을_저장한다(생성된_러너_게시글_식별자값); + + NotificationUpdateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .알림_읽음_여부_기록_업데이트를_요청한다(저장된_알림.getId()) + + .서버_응답() + .알림_읽음_여부_기록_업데이트_성공을_검증한다(); + } + + private Notification 러너_게시글_작성자에게_알림을_저장한다(final Long 생성된_러너_게시글_식별자값) { + final Member 헤나_사용자 = memberRepository.getAsRunnerByRunnerPostId(생성된_러너_게시글_식별자값); + + final Notification 저장되지_않은_알림 = Notification.builder() + .notificationTitle(new NotificationTitle("알림 테스트용 제목")) + .notificationMessage(new NotificationMessage("알림 테스트용 내용")) + .notificationType(NotificationType.RUNNER_POST) + .notificationReferencedId(new NotificationReferencedId(1L)) + .isRead(IsRead.asUnRead()) + .member(헤나_사용자) + .build(); + + return notificationCommandRepository.save(저장되지_않은_알림); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/notification/query/NotificationQueryAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/notification/query/NotificationQueryAssuredTest.java new file mode 100644 index 000000000..e492d1a2d --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/notification/query/NotificationQueryAssuredTest.java @@ -0,0 +1,91 @@ +package touch.baton.assure.notification.query; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.notification.support.query.NotificationQuerySupport; +import touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.notification.command.Notification; +import touch.baton.domain.notification.command.vo.NotificationMessage; +import touch.baton.domain.notification.command.vo.NotificationReferencedId; +import touch.baton.domain.notification.command.vo.NotificationTitle; +import touch.baton.domain.notification.command.vo.NotificationType; +import touch.baton.domain.notification.command.vo.IsRead; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostCreateRequest; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +class NotificationQueryAssuredTest extends AssuredTestConfig { + + @Test + void 로그인한_사용자의_알림_목록을_조회한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest( + "테스트용 게시글 제목", + List.of("테스트용 태그1", "테스트용 태그2"), + "https://github.com/test", + LocalDateTime.now().plusDays(10), + "테스트용 구현 내용", + "테스트용 궁금한 내용", + "테스트용 남길 내용" + ); + + final Long 생성된_러너_게시글_식별자값 = RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다(게시글_생성_요청) + + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); + + // when & then + final List 예상된_알림_목록 = 러너_게시글_작성자에게_5개의_알림을_저장한다(생성된_러너_게시글_식별자값); + + NotificationQuerySupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .로그인한_사용자의_알림_목록을_조회한다() + + .서버_응답() + .로그인한_사용자의_알림_목록_조회_성공을_검증한다(예상된_알림_목록); + } + + private List 러너_게시글_작성자에게_5개의_알림을_저장한다(final Long 생성된_러너_게시글_식별자값) { + final List 예상된_알림_목록 = new ArrayList<>(); + for (int 저장될_알림_카운트_수 = 1; 저장될_알림_카운트_수 <= 5; 저장될_알림_카운트_수++) { + final Notification 저장된_알림 = 러너_게시글_작성자에게_알림을_저장한다(생성된_러너_게시글_식별자값); + 예상된_알림_목록.add(저장된_알림); + } + Collections.sort(예상된_알림_목록, 알림_식별자값을_기준_내림차순으로_정렬한다()); + + return 예상된_알림_목록; + } + + private Notification 러너_게시글_작성자에게_알림을_저장한다(final Long 생성된_러너_게시글_식별자값) { + final Member 헤나_사용자 = memberRepository.getAsRunnerByRunnerPostId(생성된_러너_게시글_식별자값); + + final Notification 저장되지_않은_알림 = Notification.builder() + .notificationTitle(new NotificationTitle("알림 테스트용 제목")) + .notificationMessage(new NotificationMessage("알림 테스트용 내용")) + .notificationType(NotificationType.RUNNER_POST) + .notificationReferencedId(new NotificationReferencedId(1L)) + .isRead(IsRead.asUnRead()) + .member(헤나_사용자) + .build(); + + return notificationCommandRepository.save(저장되지_않은_알림); + } + + private Comparator 알림_식별자값을_기준_내림차순으로_정렬한다() { + return (left, right) -> left.getId() < right.getId() ? 1 : -1; + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/notification/support/command/NotificationDeleteSupport.java b/backend/baton/src/test/java/touch/baton/assure/notification/support/command/NotificationDeleteSupport.java new file mode 100644 index 000000000..2cc3b03f6 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/notification/support/command/NotificationDeleteSupport.java @@ -0,0 +1,59 @@ +package touch.baton.assure.notification.support.command; + +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 java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("NonAsciiCharacters") +public class NotificationDeleteSupport { + + private NotificationDeleteSupport() { + } + + public static NotificationDeleteBuilder 클라이언트_요청() { + return new NotificationDeleteBuilder(); + } + + public static class NotificationDeleteBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public NotificationDeleteBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; + return this; + } + + public NotificationDeleteBuilder 알림_삭제에_성공한다(final Long 알림_식별자값) { + response = AssuredSupport.delete("/api/v1/notifications/{notificationId}", + accessToken, + new PathParams(Map.of("notificationId", 알림_식별자값)) + ); + return this; + } + + public NotificationDeleteResponseBuilder 서버_응답() { + return new NotificationDeleteResponseBuilder(response); + } + } + + public static class NotificationDeleteResponseBuilder { + + private final ExtractableResponse response; + + public NotificationDeleteResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + + public void 알림_삭제_성공을_검증한다() { + assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/notification/support/command/NotificationUpdateSupport.java b/backend/baton/src/test/java/touch/baton/assure/notification/support/command/NotificationUpdateSupport.java new file mode 100644 index 000000000..685668065 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/notification/support/command/NotificationUpdateSupport.java @@ -0,0 +1,59 @@ +package touch.baton.assure.notification.support.command; + +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 java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("NonAsciiCharacters") +public class NotificationUpdateSupport { + + private NotificationUpdateSupport() { + } + + public static NotificationUpdateBuilder 클라이언트_요청() { + return new NotificationUpdateBuilder(); + } + + public static class NotificationUpdateBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public NotificationUpdateBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; + return this; + } + + public NotificationUpdateBuilder 알림_읽음_여부_기록_업데이트를_요청한다(final Long 알림_식별자값) { + response = AssuredSupport.patch("/api/v1/notifications/{notificationId}", + accessToken, + new PathParams(Map.of("notificationId", 알림_식별자값)) + ); + return this; + } + + public NotificationUpdateResponseBuilder 서버_응답() { + return new NotificationUpdateResponseBuilder(response); + } + } + + public static class NotificationUpdateResponseBuilder { + + private final ExtractableResponse response; + + public NotificationUpdateResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + + public void 알림_읽음_여부_기록_업데이트_성공을_검증한다() { + assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/notification/support/query/NotificationQuerySupport.java b/backend/baton/src/test/java/touch/baton/assure/notification/support/query/NotificationQuerySupport.java new file mode 100644 index 000000000..bcb843af1 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/notification/support/query/NotificationQuerySupport.java @@ -0,0 +1,60 @@ +package touch.baton.assure.notification.support.query; + +import io.restassured.common.mapper.TypeRef; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import touch.baton.assure.common.AssuredSupport; +import touch.baton.domain.notification.command.Notification; +import touch.baton.domain.notification.query.controller.response.NotificationResponses; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("NonAsciiCharacters") +public class NotificationQuerySupport { + + private NotificationQuerySupport() { + } + + public static NotificationQueryBuilder 클라이언트_요청() { + return new NotificationQueryBuilder(); + } + + public static class NotificationQueryBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public NotificationQueryBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; + return this; + } + + public NotificationQueryBuilder 로그인한_사용자의_알림_목록을_조회한다() { + response = AssuredSupport.get("/api/v1/notifications", accessToken); + return this; + } + + public NotificationQueryResponseBuilder 서버_응답() { + return new NotificationQueryResponseBuilder(response); + } + } + + public static class NotificationQueryResponseBuilder { + + private final ExtractableResponse response; + + public NotificationQueryResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + + public void 로그인한_사용자의_알림_목록_조회_성공을_검증한다(final List 알림_목록) { + final NotificationResponses.SimpleNotifications 조회된_알림_응답_목록 = this.response.as(new TypeRef<>() { + }); + + assertThat(조회된_알림_응답_목록).isEqualTo(NotificationResponses.SimpleNotifications.from(알림_목록)); + } + } +} 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 index a416eb065..a04fc622f 100644 --- a/backend/baton/src/test/java/touch/baton/assure/oauth/OauthAssuredSupport.java +++ b/backend/baton/src/test/java/touch/baton/assure/oauth/OauthAssuredSupport.java @@ -8,13 +8,13 @@ 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 touch.baton.domain.member.command.Member; +import touch.baton.domain.oauth.command.OauthType; +import touch.baton.domain.oauth.command.token.AccessToken; +import touch.baton.domain.oauth.command.token.ExpireDate; +import touch.baton.domain.oauth.command.token.RefreshToken; +import touch.baton.domain.oauth.command.token.Token; +import touch.baton.domain.oauth.command.token.Tokens; import java.time.LocalDateTime; import java.util.Map; @@ -91,6 +91,18 @@ public static class OauthClientRequestBuilder { return this; } + public OauthClientRequestBuilder 로그아웃을_요청한다(final AccessToken 액세스_토큰) { + response = AssuredSupport.patch("/api/v1/oauth/logout", 액세스_토큰.getValue()); + + return this; + } + + public OauthClientRequestBuilder 액세스_토큰_없이_로그아웃을_요청한다() { + response = AssuredSupport.patch("/api/v1/oauth/logout"); + + return this; + } + public OauthServerResponseBuilder 서버_응답() { return new OauthServerResponseBuilder(response); } @@ -153,5 +165,11 @@ public OauthServerResponseBuilder(final ExtractableResponse response) softly.assertThat(response.jsonPath().getString("errorCode")).isEqualTo(clientErrorCode.getErrorCode()); }); } + + public void 로그아웃이_성공한다() { + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + }); + } } } 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 index 557c24292..b406c9c5e 100644 --- a/backend/baton/src/test/java/touch/baton/assure/oauth/OauthCreateAssuredTest.java +++ b/backend/baton/src/test/java/touch/baton/assure/oauth/OauthCreateAssuredTest.java @@ -2,8 +2,8 @@ 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; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.oauth.command.OauthType; @SuppressWarnings("NonAsciiCharacters") class OauthCreateAssuredTest extends AssuredTestConfig { @@ -19,7 +19,7 @@ class OauthCreateAssuredTest extends AssuredTestConfig { OauthAssuredSupport .클라이언트_요청() - .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, MockAuthCodes.hyenaAuthCode()) + .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, FakeAuthCodes.hyenaAuthCode()) .서버_응답() .AuthCode를_통해_소셜_토큰_발급_및_사용자_회원가입에_성공한다(); diff --git a/backend/baton/src/test/java/touch/baton/assure/oauth/OauthDeleteAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/oauth/OauthDeleteAssuredTest.java new file mode 100644 index 000000000..adb556eef --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/oauth/OauthDeleteAssuredTest.java @@ -0,0 +1,67 @@ +package touch.baton.assure.oauth; + +import org.junit.jupiter.api.Test; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.oauth.command.OauthType; +import touch.baton.domain.oauth.command.token.Tokens; +import touch.baton.fixture.domain.MemberFixture; + +@SuppressWarnings("NonAsciiCharacters") +class OauthDeleteAssuredTest extends AssuredTestConfig { + + @Test + void 로그아웃을_성공한다() { + // given + OauthAssuredSupport + .클라이언트_요청() + .소셜_로그인을_위한_리다이렉트_URL을_요청한다(OauthType.GITHUB) + + .서버_응답() + .소셜_로그인을_위한_리다이렉트_URL_요청_성공을_검증한다(); + + final Tokens 액세스_토큰과_리프레시_토큰 = OauthAssuredSupport + .클라이언트_요청() + .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, FakeAuthCodes.ethanAuthCode()) + + .서버_응답() + .AuthCode를_통해_소셜_토큰_발급_및_사용자_회원가입에_성공한다() + .액세스_토큰과_리프레시_토큰을_반환한다(MemberFixture.createEthan()); + + // when, then + OauthAssuredSupport + .클라이언트_요청() + .로그아웃을_요청한다(액세스_토큰과_리프레시_토큰.accessToken()) + + .서버_응답() + .로그아웃이_성공한다(); + } + + @Test + void 액세스_토큰이_없이_로그아웃을_요청하면_실패한다() { + // given + OauthAssuredSupport + .클라이언트_요청() + .소셜_로그인을_위한_리다이렉트_URL을_요청한다(OauthType.GITHUB) + + .서버_응답() + .소셜_로그인을_위한_리다이렉트_URL_요청_성공을_검증한다(); + + final Tokens 액세스_토큰과_리프레시_토큰 = OauthAssuredSupport + .클라이언트_요청() + .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, FakeAuthCodes.ethanAuthCode()) + + .서버_응답() + .AuthCode를_통해_소셜_토큰_발급_및_사용자_회원가입에_성공한다() + .액세스_토큰과_리프레시_토큰을_반환한다(MemberFixture.createEthan()); + + // when, then + OauthAssuredSupport + .클라이언트_요청() + .액세스_토큰_없이_로그아웃을_요청한다() + + .서버_응답() + .오류가_발생한다(ClientErrorCode.OAUTH_AUTHORIZATION_VALUE_IS_NULL); + } +} 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 index 3b0d97b2a..2a936f786 100644 --- a/backend/baton/src/test/java/touch/baton/assure/oauth/OauthRefreshTokenAssuredTest.java +++ b/backend/baton/src/test/java/touch/baton/assure/oauth/OauthRefreshTokenAssuredTest.java @@ -3,13 +3,13 @@ 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.config.infra.auth.oauth.authcode.FakeAuthCodes; 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.domain.member.command.vo.SocialId; +import touch.baton.domain.oauth.command.OauthType; +import touch.baton.domain.oauth.command.token.ExpireDate; +import touch.baton.domain.oauth.command.token.Token; +import touch.baton.domain.oauth.command.token.Tokens; import touch.baton.fixture.domain.MemberFixture; import touch.baton.fixture.vo.ExpireDateFixture; import touch.baton.infra.auth.jwt.JwtEncoder; @@ -34,7 +34,7 @@ class OauthRefreshTokenAssuredTest extends AssuredTestConfig { final Tokens 액세스_토큰과_리프레시_토큰 = OauthAssuredSupport .클라이언트_요청() - .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, MockAuthCodes.ethanAuthCode()) + .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, FakeAuthCodes.ethanAuthCode()) .서버_응답() .AuthCode를_통해_소셜_토큰_발급_및_사용자_회원가입에_성공한다() @@ -59,7 +59,7 @@ class OauthRefreshTokenAssuredTest extends AssuredTestConfig { final Tokens 액세스_토큰과_리프레시_토큰 = OauthAssuredSupport .클라이언트_요청() - .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, MockAuthCodes.ethanAuthCode()) + .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, FakeAuthCodes.ethanAuthCode()) .서버_응답() .AuthCode를_통해_소셜_토큰_발급_및_사용자_회원가입에_성공한다() @@ -85,7 +85,7 @@ class OauthRefreshTokenAssuredTest extends AssuredTestConfig { final Tokens 액세스_토큰과_리프레시_토큰 = OauthAssuredSupport .클라이언트_요청() - .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, MockAuthCodes.ethanAuthCode()) + .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, FakeAuthCodes.ethanAuthCode()) .서버_응답() .AuthCode를_통해_소셜_토큰_발급_및_사용자_회원가입에_성공한다() @@ -113,7 +113,7 @@ class OauthRefreshTokenAssuredTest extends AssuredTestConfig { final Tokens 액세스_토큰과_리프레시_토큰 = OauthAssuredSupport .클라이언트_요청() - .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, MockAuthCodes.ethanAuthCode()) + .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, FakeAuthCodes.ethanAuthCode()) .서버_응답() .AuthCode를_통해_소셜_토큰_발급_및_사용자_회원가입에_성공한다() @@ -141,7 +141,7 @@ class OauthRefreshTokenAssuredTest extends AssuredTestConfig { final Tokens 헤나_액세스_토큰과_리프레시_토큰 = OauthAssuredSupport .클라이언트_요청() - .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, MockAuthCodes.hyenaAuthCode()) + .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, FakeAuthCodes.hyenaAuthCode()) .서버_응답() .AuthCode를_통해_소셜_토큰_발급_및_사용자_회원가입에_성공한다() @@ -149,7 +149,7 @@ class OauthRefreshTokenAssuredTest extends AssuredTestConfig { final Tokens 에단_액세스_토큰과_리프레시_토큰 = OauthAssuredSupport .클라이언트_요청() - .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, MockAuthCodes.ethanAuthCode()) + .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, FakeAuthCodes.ethanAuthCode()) .서버_응답() .AuthCode를_통해_소셜_토큰_발급_및_사용자_회원가입에_성공한다() @@ -181,7 +181,7 @@ class OauthRefreshTokenAssuredTest extends AssuredTestConfig { final Tokens 액세스_토큰과_리프레시_토큰 = OauthAssuredSupport .클라이언트_요청() - .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, MockAuthCodes.ethanAuthCode()) + .AuthCode를_통해_소셜_토큰을_발급_받은_후_사용자를_회원가입_한다(OauthType.GITHUB, FakeAuthCodes.ethanAuthCode()) .서버_응답() .AuthCode를_통해_소셜_토큰_발급_및_사용자_회원가입에_성공한다() diff --git a/backend/baton/src/test/java/touch/baton/assure/repository/TestMemberQueryRepository.java b/backend/baton/src/test/java/touch/baton/assure/repository/TestMemberQueryRepository.java new file mode 100644 index 000000000..e7e0b8f70 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/repository/TestMemberQueryRepository.java @@ -0,0 +1,33 @@ +package touch.baton.assure.repository; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.repository.MemberCommandRepository; +import touch.baton.domain.member.command.vo.SocialId; + +import java.util.Optional; + +public interface TestMemberQueryRepository extends MemberCommandRepository { + + default Member getBySocialId(final SocialId socialId) { + return findBySocialId(socialId) + .orElseThrow(() -> new IllegalArgumentException("테스트에서 Member 를 SocialId 로 조회할 수 없습니다.")); + }; + + Optional findBySocialId(final SocialId socialId); + + default Member getAsRunnerByRunnerPostId(final Long runnerPostId) { + return findAsRunnerByRunnerPostId(runnerPostId) + .orElseThrow(() -> new IllegalArgumentException("테스트에서 Member 를 runnerPostId로 러너 게시글의 작성자(Runner)로서 조회할 수 없습니다.")); + }; + + @Query(""" + select rp.runner.member + from RunnerPost rp + join fetch Runner r on r.id = rp.runner.id + join fetch Member m on m.id = r.member.id + where rp.id = :runnerPostId + """) + Optional findAsRunnerByRunnerPostId(@Param("runnerPostId") final Long runnerPostId); +} diff --git a/backend/baton/src/test/java/touch/baton/assure/repository/TestNotificationCommandRepository.java b/backend/baton/src/test/java/touch/baton/assure/repository/TestNotificationCommandRepository.java new file mode 100644 index 000000000..431040f04 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/repository/TestNotificationCommandRepository.java @@ -0,0 +1,6 @@ +package touch.baton.assure.repository; + +import touch.baton.domain.notification.command.repository.NotificationCommandRepository; + +public interface TestNotificationCommandRepository extends NotificationCommandRepository { +} 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 index 7f37c856c..d19ddd643 100644 --- a/backend/baton/src/test/java/touch/baton/assure/repository/TestRefreshTokenRepository.java +++ b/backend/baton/src/test/java/touch/baton/assure/repository/TestRefreshTokenRepository.java @@ -5,9 +5,9 @@ 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; +import touch.baton.domain.oauth.command.token.ExpireDate; +import touch.baton.domain.oauth.command.token.RefreshToken; +import touch.baton.domain.oauth.command.token.Token; public interface TestRefreshTokenRepository extends JpaRepository { diff --git a/backend/baton/src/test/java/touch/baton/assure/repository/TestRunnerPostQueryRepository.java b/backend/baton/src/test/java/touch/baton/assure/repository/TestRunnerPostQueryRepository.java new file mode 100644 index 000000000..8c96a10ca --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/repository/TestRunnerPostQueryRepository.java @@ -0,0 +1,27 @@ +package touch.baton.assure.repository; + +import org.springframework.context.annotation.Profile; +import org.springframework.data.repository.query.Param; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.repository.dto.RunnerPostApplicantCountDto; +import touch.baton.domain.runnerpost.query.repository.RunnerPostQueryRepository; + +import java.util.List; + +@Profile("test") +public interface TestRunnerPostQueryRepository extends RunnerPostQueryRepository { + + default RunnerPost getByRunnerPostId(@Param("runnerPostId") final Long runnerPostId) { + return joinMemberByRunnerPostId(runnerPostId) + .orElseThrow(() -> new IllegalArgumentException("테스트에서 RunnerPost 를 러너 게시글 식별자값(id) 로 조회할 수 없습니다.")); + } + + 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/TestRunnerQueryRepository.java b/backend/baton/src/test/java/touch/baton/assure/repository/TestRunnerQueryRepository.java new file mode 100644 index 000000000..e7719f7f5 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/repository/TestRunnerQueryRepository.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.command.Runner; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.member.query.repository.RunnerQueryRepository; + +import java.util.Optional; + +public interface TestRunnerQueryRepository extends RunnerQueryRepository { + + 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/TestSupporterQueryRepository.java b/backend/baton/src/test/java/touch/baton/assure/repository/TestSupporterQueryRepository.java new file mode 100644 index 000000000..bb152099f --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/repository/TestSupporterQueryRepository.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.command.Supporter; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.member.query.repository.SupporterQueryRepository; + +import java.util.Optional; + +public interface TestSupporterQueryRepository extends SupporterQueryRepository { + + 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/TestSupporterRunnerPostQueryRepository.java b/backend/baton/src/test/java/touch/baton/assure/repository/TestSupporterRunnerPostQueryRepository.java new file mode 100644 index 000000000..a77e69712 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/repository/TestSupporterRunnerPostQueryRepository.java @@ -0,0 +1,12 @@ +package touch.baton.assure.repository; + +import org.springframework.context.annotation.Profile; +import touch.baton.domain.member.query.repository.SupporterRunnerPostQueryRepository; + +@Profile("test") +public interface TestSupporterRunnerPostQueryRepository extends SupporterRunnerPostQueryRepository { + + default Long getApplicantCountByRunnerPostId(final Long runnerPostId) { + return countByRunnerPostId(runnerPostId).orElse(0L); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/repository/TestTagQuerydslRepository.java b/backend/baton/src/test/java/touch/baton/assure/repository/TestTagQuerydslRepository.java new file mode 100644 index 000000000..d1f557b8f --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/repository/TestTagQuerydslRepository.java @@ -0,0 +1,13 @@ +package touch.baton.assure.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.springframework.stereotype.Repository; +import touch.baton.domain.tag.query.repository.TagQuerydslRepository; + +@Repository +public class TestTagQuerydslRepository extends TagQuerydslRepository { + + public TestTagQuerydslRepository(final JPAQueryFactory queryFactory) { + super(queryFactory); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/repository/TestTechnicalTagQueryRepository.java b/backend/baton/src/test/java/touch/baton/assure/repository/TestTechnicalTagQueryRepository.java new file mode 100644 index 000000000..b5c6d2172 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/repository/TestTechnicalTagQueryRepository.java @@ -0,0 +1,6 @@ +package touch.baton.assure.repository; + +import touch.baton.domain.technicaltag.query.repository.TechnicalTagQueryRepository; + +public interface TestTechnicalTagQueryRepository extends TechnicalTagQueryRepository { +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/command/RunnerPostCreateAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/command/RunnerPostCreateAssuredTest.java new file mode 100644 index 000000000..a11bb1ab2 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/command/RunnerPostCreateAssuredTest.java @@ -0,0 +1,258 @@ +package touch.baton.assure.runnerpost.command; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.common.HttpStatusAndLocationHeader; +import touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.common.response.ErrorResponse; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostCreateRequest; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.springframework.http.HttpStatus.CREATED; + +@SuppressWarnings("NonAsciiCharacters") +class RunnerPostCreateAssuredTest extends AssuredTestConfig { + + @Test + void 러너_게시글_등록이_성공한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest("코드 리뷰 해주세요.", + List.of("Java", "Spring"), + "https://github.com/cookienc", + LocalDateTime.now().plusDays(10), + "싸게 부탁드려요.", + "이거 궁금해요.", + "잘 부탁드립니다." + ); + + // when, then + RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다(게시글_생성_요청) + + .서버_응답() + .러너_게시글_등록_성공을_검증한다(new HttpStatusAndLocationHeader(CREATED, "/api/v1/posts/runner")); + } + + @Test + void 게시글_제목이_null이면_러너_게시글_등록_실패한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest(null, + List.of("Java", "Spring"), + "https://github.com/cookienc", + LocalDateTime.now().plusDays(10), + "싸게 부탁드려요.", + "이거 궁금해요.", + "잘 부탁드립니다." + ); + + // when, then + RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다(게시글_생성_요청) + + .서버_응답() + .러너_게시글_등록_실패를_검증한다(new ErrorResponse("RP001", "제목을 입력해주세요.")); + } + + @Test + void 게시글_태그가_null이면_러너_게시글_등록_실패한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest("코드 리뷰 해주세요.", + null, + "https://github.com/cookienc", + LocalDateTime.now().plusDays(10), + "싸게 부탁드려요.", + "이거 궁금해요.", + "잘 부탁드립니다." + ); + + // when, then + RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다(게시글_생성_요청) + + .서버_응답() + .러너_게시글_등록_실패를_검증한다(new ErrorResponse("RP008", "태그 목록을 빈 값이라도 입력해주세요.")); + } + + @Test + void 게시글_PR_URL이_null이면_러너_게시글_등록_실패한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest("코드 리뷰 해주세요.", + List.of("Java", "Spring"), + null, + LocalDateTime.now().plusDays(10), + "싸게 부탁드려요.", + "이거 궁금해요.", + "잘 부탁드립니다." + ); + + // when, then + RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다(게시글_생성_요청) + + .서버_응답() + .러너_게시글_등록_실패를_검증한다(new ErrorResponse("RP002", "PR 주소를 입력해주세요.")); + } + + @Test + void 게시글_마감기한이_null이면_러너_게시글_등록_실패한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest("코드 리뷰 해주세요.", + List.of("Java", "Spring"), + "https://github.com/cookienc", + null, + "싸게 부탁드려요.", + "이거 궁금해요.", + "잘 부탁드립니다." + ); + + // when, then + RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다(게시글_생성_요청) + + .서버_응답() + .러너_게시글_등록_실패를_검증한다(new ErrorResponse("RP003", "마감일을 입력해주세요.")); + } + + @Test + void 게시글_마감기한이_현재보다_과거면_러너_게시글_등록_실패한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest("코드 리뷰 해주세요.", + List.of("Java", "Spring"), + "https://github.com/cookienc", + LocalDateTime.now().minusDays(1), + "싸게 부탁드려요.", + "이거 궁금해요.", + "잘 부탁드립니다." + ); + + // when, then + RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다(게시글_생성_요청) + + .서버_응답() + .러너_게시글_등록_실패를_검증한다(new ErrorResponse("RP006", "마감일은 오늘보다 과거일 수 없습니다.")); + } + + @Test + void 구현_내용이_null이면_러너_게시글_등록_실패한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest("코드 리뷰 해주세요.", + List.of("Java", "Spring"), + "https://github.com/cookienc", + LocalDateTime.now().plusDays(10), + null, + "이거 궁금해요.", + "잘 부탁드립니다." + ); + + // when, then + RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다(게시글_생성_요청) + + .서버_응답() + .러너_게시글_등록_실패를_검증한다(new ErrorResponse("RP004", "구현 내용을 입력해주세요.")); + } + + @Test + void 궁금한_내용이_null이면_러너_게시글_등록_실패한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest("코드 리뷰 해주세요.", + List.of("Java", "Spring"), + "https://github.com/cookienc", + LocalDateTime.now().plusDays(10), + "이거 해줘요.", + null, + "잘 부탁드립니다." + ); + + // when, then + RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다(게시글_생성_요청) + + .서버_응답() + .러너_게시글_등록_실패를_검증한다(new ErrorResponse("RP012", "궁금한 내용을 입력해주세요.")); + } + + @Test + void 참고_사항이_null이면_러너_게시글_등록_실패한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest("코드 리뷰 해주세요.", + List.of("Java", "Spring"), + "https://github.com/cookienc", + LocalDateTime.now().plusDays(10), + "잘 부탁드립니다.", + "이거 궁금해요.", + null + ); + + // when, then + RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다(게시글_생성_요청) + + .서버_응답() + .러너_게시글_등록_실패를_검증한다(new ErrorResponse("RP013", "참고 사항을 입력해주세요.")); + } + + @Test + void 게시글_내용이_1000자_보다_길면_러너_게시글_등록_실패한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final RunnerPostCreateRequest 게시글_생성_요청 = new RunnerPostCreateRequest("코드 리뷰 해주세요.", + List.of("Java", "Spring"), + "https://github.com/cookienc", + LocalDateTime.now().plusDays(10), + "12345".repeat(200) + "1", + "", + "" + ); + + // when, then + RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다(게시글_생성_요청) + + .서버_응답() + .러너_게시글_등록_실패를_검증한다(new ErrorResponse("RP005", "내용은 1000자 까지 입력해주세요.")); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/command/RunnerPostDeleteAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/command/RunnerPostDeleteAssuredTest.java new file mode 100644 index 000000000..45fdf19b1 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/command/RunnerPostDeleteAssuredTest.java @@ -0,0 +1,174 @@ +package touch.baton.assure.runnerpost.command; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import touch.baton.assure.common.HttpStatusAndLocationHeader; +import touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport; +import touch.baton.assure.runnerpost.support.command.RunnerPostDeleteSupport; +import touch.baton.assure.runnerpost.support.command.RunnerPostUpdateSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.vo.SocialId; + +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.assure.runnerpost.support.command.RunnerPostCreateSupport.러너_게시글_생성_요청; +import static touch.baton.assure.runnerpost.support.command.applicant.RunnerPostApplicantCreateSupport.러너의_서포터_선택_요청; +import static touch.baton.assure.runnerpost.support.command.applicant.RunnerPostApplicantCreateSupport.클라이언트_요청; + +@SuppressWarnings("NonAsciiCharacters") +class RunnerPostDeleteAssuredTest extends AssuredTestConfig { + + @Test + void 리뷰가_대기중이고_리뷰_지원자가_없다면_러너의_게시글_식별자값으로_러너_게시글_삭제에_성공한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + final Long 러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(헤나_액세스_토큰); + + // when, then + RunnerPostDeleteSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_식별자값으로_러너_게시글을_삭제한다(러너_게시글_식별자값) + + .서버_응답() + .러너_게시글_삭제_성공을_검증한다(NO_CONTENT); + } + + @Test + void 러너_게시글이_존재하지_않으면_러너의_게시글_식별자값으로_러너_게시글_삭제에_실패한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + // when + final Long 존재하지_않는_러너_게시글의_식별자값 = -1L; + + // then + RunnerPostDeleteSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_식별자값으로_러너_게시글을_삭제한다(존재하지_않는_러너_게시글의_식별자값) + + .서버_응답() + .러너_게시글_삭제_실패를_검증한다(INTERNAL_SERVER_ERROR); + } + + @Test + void 리뷰가_진행중인_상태라면_러너의_게시글_식별자값으로_러너_게시글_삭제에_실패한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + + // then + RunnerPostDeleteSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_식별자값으로_러너_게시글을_삭제한다(디투_러너_게시글_식별자값) + + .서버_응답() + .러너_게시글_삭제_실패를_검증한다(INTERNAL_SERVER_ERROR); + } + + @Test + void 리뷰가_완료된_상태라면_러너의_게시글_식별자값으로_러너_게시글_삭제에_실패한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Supporter 서포터_헤나 = supporterRepository.getBySocialId(헤나_소셜_아이디); + + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + 러너가_서포터의_리뷰_신청_선택에_성공한다(서포터_헤나, 디투_액세스_토큰, 디투_러너_게시글_식별자값); + 서포터가_러너_게시글의_리뷰를_완료로_변경하는_것을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + + // then + RunnerPostDeleteSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_식별자값으로_러너_게시글을_삭제한다(디투_러너_게시글_식별자값) + + .서버_응답() + .러너_게시글_삭제_실패를_검증한다(INTERNAL_SERVER_ERROR); + } + + @Test + void 리뷰_요청_대기중인_상태이고_리뷰_지원자가_있는_상태라면_러너의_게시글_식별자값으로_러너_게시글_삭제에_실패한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + + // then + RunnerPostDeleteSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_식별자값으로_러너_게시글을_삭제한다(디투_러너_게시글_식별자값) + + .서버_응답() + .러너_게시글_삭제_실패를_검증한다(INTERNAL_SERVER_ERROR); + } + + private Long 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(final String 헤나_액세스_토큰) { + return RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다( + 러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + List.of("자바", "스프링"), + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + ) + ) + + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); + } + + private void 서포터가_러너_게시글에_리뷰_신청을_성공한다(final String 헤나_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + 클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .서포터가_러너_게시글에_리뷰를_신청한다(디투_러너_게시글_식별자값, "안녕하세요. 서포터 헤나입니다.") + + .서버_응답() + .서포터가_러너_게시글에_리뷰_신청_성공을_검증한다(디투_러너_게시글_식별자값); + } + + private void 러너가_서포터의_리뷰_신청_선택에_성공한다(final Supporter 서포터_헤나, final String 디투_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + RunnerPostUpdateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(디투_액세스_토큰) + .러너가_서포터를_선택한다(디투_러너_게시글_식별자값, 러너의_서포터_선택_요청(서포터_헤나.getId())) + + .서버_응답() + .러너_게시글에_서포터가_성공적으로_선택되었는지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); + } + + private void 서포터가_러너_게시글의_리뷰를_완료로_변경하는_것을_성공한다(final String 헤나_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + RunnerPostUpdateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .서포터가_리뷰를_완료하고_리뷰완료_버튼을_누른다(디투_러너_게시글_식별자값) + + .서버_응답() + .러너_게시글이_성공적으로_리뷰_완료_상태인지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/command/RunnerPostUpdateAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/command/RunnerPostUpdateAssuredTest.java new file mode 100644 index 000000000..2b533ffa1 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/command/RunnerPostUpdateAssuredTest.java @@ -0,0 +1,113 @@ +package touch.baton.assure.runnerpost.command; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import touch.baton.assure.common.HttpStatusAndLocationHeader; +import touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport; +import touch.baton.assure.runnerpost.support.command.RunnerPostUpdateSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostUpdateRequest; + +import java.time.LocalDateTime; +import java.util.List; + +import static touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport.러너_게시글_생성_요청; +import static touch.baton.assure.runnerpost.support.command.applicant.RunnerPostApplicantCreateSupport.러너의_서포터_선택_요청; +import static touch.baton.assure.runnerpost.support.command.applicant.RunnerPostApplicantCreateSupport.클라이언트_요청; + +@SuppressWarnings("NonAsciiCharacters") +class RunnerPostUpdateAssuredTest extends AssuredTestConfig { + + @Test + void 러너가_서포터_목록에서_서포터를_선택할_수_있다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Supporter 서포터_헤나 = supporterRepository.getBySocialId(헤나_소셜_아이디); + + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + + // then + final RunnerPostUpdateRequest.SelectSupporter 러너의_서포터_선택_요청 = 러너의_서포터_선택_요청(서포터_헤나.getId()); + + RunnerPostUpdateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(디투_액세스_토큰) + .러너가_서포터를_선택한다(디투_러너_게시글_식별자값, 러너의_서포터_선택_요청) + + .서버_응답() + .러너_게시글에_서포터가_성공적으로_선택되었는지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); + } + + @Test + void 서포터_리뷰완료_후_리뷰상태를_완료로_변경할_수_있다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Supporter 서포터_헤나 = supporterRepository.getBySocialId(헤나_소셜_아이디); + + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + 러너가_서포터의_리뷰_신청_선택에_성공한다(서포터_헤나, 디투_액세스_토큰, 디투_러너_게시글_식별자값); + + // when, then + RunnerPostUpdateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .서포터가_리뷰를_완료하고_리뷰완료_버튼을_누른다(디투_러너_게시글_식별자값) + + .서버_응답() + .러너_게시글이_성공적으로_리뷰_완료_상태인지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); + } + + + private Long 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(final String 헤나_액세스_토큰) { + return RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다( + 러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + List.of("자바", "스프링"), + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + ) + ) + + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); + } + + private void 서포터가_러너_게시글에_리뷰_신청을_성공한다(final String 헤나_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + 클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .서포터가_러너_게시글에_리뷰를_신청한다(디투_러너_게시글_식별자값, "안녕하세요. 서포터 헤나입니다.") + + .서버_응답() + .서포터가_러너_게시글에_리뷰_신청_성공을_검증한다(디투_러너_게시글_식별자값); + } + + private void 러너가_서포터의_리뷰_신청_선택에_성공한다(final Supporter 서포터_헤나, final String 디투_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + RunnerPostUpdateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(디투_액세스_토큰) + .러너가_서포터를_선택한다(디투_러너_게시글_식별자값, 러너의_서포터_선택_요청(서포터_헤나.getId())) + + .서버_응답() + .러너_게시글에_서포터가_성공적으로_선택되었는지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/command/applicant/RunnerPostApplicantCreateAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/command/applicant/RunnerPostApplicantCreateAssuredTest.java new file mode 100644 index 000000000..6e026ec93 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/command/applicant/RunnerPostApplicantCreateAssuredTest.java @@ -0,0 +1,97 @@ +package touch.baton.assure.runnerpost.command.applicant; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport; +import touch.baton.assure.runnerpost.support.command.applicant.RunnerPostApplicantCreateSupport; +import touch.baton.assure.runnerpost.support.query.detail.RunnerPostDetailSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostCreateRequest; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.query.controller.response.RunnerPostResponse; + +import java.util.List; + +import static java.time.LocalDateTime.now; +import static touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport.러너_게시글_생성_요청; +import static touch.baton.assure.runnerpost.support.query.detail.RunnerPostDetailSupport.러너_게시글_Detail_응답; +import static touch.baton.domain.runnerpost.command.vo.ReviewStatus.NOT_STARTED; + +@SuppressWarnings("NonAsciiCharacters") +class RunnerPostApplicantCreateAssuredTest extends AssuredTestConfig { + + @Test + void 러너가_러너_게시글을_생성하고_서포터가_러너_게시글에_리뷰를_신청한다() { + final String 에단_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ethanAuthCode()); + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final RunnerPostCreateRequest 러너_게시글_생성_요청 = 러너_게시글_생성_요청을_생성한다(); + final Long 에단의_러너_게시글_식별자값 = RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(에단_액세스_토큰) + .러너_게시글_등록_요청한다(러너_게시글_생성_요청) + + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); + + final SocialId 에단의_소셜_아이디 = jwtTestManager.parseToSocialId(에단_액세스_토큰); + final Runner 러너_에단 = runnerRepository.getBySocialId(에단의_소셜_아이디); + + final RunnerPostResponse.Detail 리뷰가_시작되지_않은_에단의_러너_게시글_Detail_응답 = 러너_게시글_Detail_응답을_생성한다(러너_에단, 러너_게시글_생성_요청, NOT_STARTED, 에단의_러너_게시글_식별자값, 1, 0L, false); + RunnerPostDetailSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(에단_액세스_토큰) + .러너_게시글_식별자값으로_러너_게시글을_조회한다(에단의_러너_게시글_식별자값) + + .서버_응답() + .러너_게시글_단건_조회_성공을_검증한다(리뷰가_시작되지_않은_에단의_러너_게시글_Detail_응답); + + RunnerPostApplicantCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .서포터가_러너_게시글에_리뷰를_신청한다(에단의_러너_게시글_식별자값, "안녕하세요. 서포터 헤나입니다.") + + .서버_응답() + .서포터가_러너_게시글에_리뷰_신청_성공을_검증한다(에단의_러너_게시글_식별자값); + } + + private RunnerPostCreateRequest 러너_게시글_생성_요청을_생성한다() { + return 러너_게시글_생성_요청( + "러너 게시글 테스트 제목", + List.of("java", "spring"), + "https://github.com", + now().plusHours(10), + "러너 게시글 내용", + "게시글 궁금한 내용", + "참고 사항"); + } + + private RunnerPostResponse.Detail 러너_게시글_Detail_응답을_생성한다(final Runner 러너, + final RunnerPostCreateRequest 러너_게시글_생성_요청, + final ReviewStatus 리뷰_상태, + final Long 러너_게시글_식별자값, + final int 조회수, + final long 서포터_지원자수, + final boolean 서포터_지원_여부 + ) { + return 러너_게시글_Detail_응답( + 러너_게시글_식별자값, + 러너_게시글_생성_요청.title(), + 러너_게시글_생성_요청.implementedContents(), + 러너_게시글_생성_요청.curiousContents(), + 러너_게시글_생성_요청.postscriptContents(), + 러너_게시글_생성_요청.pullRequestUrl(), + 러너_게시글_생성_요청.deadline(), + 조회수, + 서포터_지원자수, + 리뷰_상태, + true, + 서포터_지원_여부, + 러너, + 러너_게시글_생성_요청.tags() + ); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/command/applicant/RunnerPostApplicantDeleteAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/command/applicant/RunnerPostApplicantDeleteAssuredTest.java new file mode 100644 index 000000000..4493b071b --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/command/applicant/RunnerPostApplicantDeleteAssuredTest.java @@ -0,0 +1,69 @@ +package touch.baton.assure.runnerpost.command.applicant; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.common.HttpStatusAndLocationHeader; +import touch.baton.assure.runnerpost.support.command.applicant.RunnerPostApplicantCreateSupport; +import touch.baton.assure.runnerpost.support.command.applicant.RunnerPostApplicantDeleteAssuredSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.springframework.http.HttpStatus.NO_CONTENT; +import static touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport.러너_게시글_생성_요청; +import static touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport.클라이언트_요청; + +@SuppressWarnings("NonAsciiCharacters") +class RunnerPostApplicantDeleteAssuredTest extends AssuredTestConfig { + + @Test + void 러너_게시글에_보낸_리뷰_제안을_취소한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + + // then + RunnerPostApplicantDeleteAssuredSupport + .클라이언트_요청() + .액세스_토큰으로_로그인_한다(헤나_액세스_토큰) + .서포터가_리뷰_제안을_취소한다(디투_러너_게시글_식별자값) + + .서버_응답() + .서포터의_리뷰_제안_철회를_검증한다(new HttpStatusAndLocationHeader(NO_CONTENT, "/api/v1/posts/runner/" + 디투_러너_게시글_식별자값)); + } + + private Long 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(final String 헤나_액세스_토큰) { + return 클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다( + 러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + List.of("자바", "스프링"), + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + ) + ) + + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); + } + + private void 서포터가_러너_게시글에_리뷰_신청을_성공한다(final String 서포터_액세스_토큰, final Long 러너_게시글_식별자값) { + RunnerPostApplicantCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(서포터_액세스_토큰) + .서포터가_러너_게시글에_리뷰를_신청한다(러너_게시글_식별자값, "안녕하세요. 서포터 헤나입니다.") + + .서버_응답() + .서포터가_러너_게시글에_리뷰_신청_성공을_검증한다(러너_게시글_식별자값); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/query/applicant/RunnerPostApplicantAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/query/applicant/RunnerPostApplicantAssuredTest.java new file mode 100644 index 000000000..8614a87db --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/query/applicant/RunnerPostApplicantAssuredTest.java @@ -0,0 +1,84 @@ +package touch.baton.assure.runnerpost.query.applicant; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport; +import touch.baton.assure.runnerpost.support.command.applicant.RunnerPostApplicantCreateSupport; +import touch.baton.assure.runnerpost.support.query.applicant.RunnerPostApplicantQuerySupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.runnerpost.query.controller.response.SupporterRunnerPostResponse; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; + +import static touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport.러너_게시글_생성_요청; +import static touch.baton.assure.runnerpost.support.query.applicant.RunnerPostApplicantQuerySupport.지원한_서포터_목록_응답; +import static touch.baton.assure.runnerpost.support.query.applicant.RunnerPostApplicantQuerySupport.지원한_서포터_응답; + +@SuppressWarnings("NonAsciiCharacters") +class RunnerPostApplicantAssuredTest extends AssuredTestConfig { + + @Test + void 러너의_게시글_식별자값으로_지원한_서포터_목록_조회에_성공한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Supporter 서포터_헤나 = supporterRepository.getBySocialId(헤나_소셜_아이디); + + final SupporterRunnerPostResponse.Detail 지원한_서포터_헤나_응답 = 지원한_서포터_응답( + 서포터_헤나, + 0, + "안녕하세요. 서포터 헤나입니다.", + Collections.emptyList() + ); + + // then + RunnerPostApplicantQuerySupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(디투_액세스_토큰) + .러너_게시글_식별자값으로_지원한_서포터_목록을_조회한다(디투_러너_게시글_식별자값) + + .서버_응답() + .지원한_서포터_목록_조회_성공을_검증한다(지원한_서포터_목록_응답(List.of(지원한_서포터_헤나_응답))); + } + + private Long 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(final String 헤나_액세스_토큰) { + return RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다( + 러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + List.of("자바", "스프링"), + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + ) + ) + + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); + } + + private void 서포터가_러너_게시글에_리뷰_신청을_성공한다(final String 서포터_액세스_토큰, final Long 러너_게시글_식별자값) { + RunnerPostApplicantCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(서포터_액세스_토큰) + .서포터가_러너_게시글에_리뷰를_신청한다(러너_게시글_식별자값, "안녕하세요. 서포터 헤나입니다.") + + .서버_응답() + .서포터가_러너_게시글에_리뷰_신청_성공을_검증한다(러너_게시글_식별자값); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/query/count/runner/RunnerPostCountByRunnerAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/query/count/runner/RunnerPostCountByRunnerAssuredTest.java new file mode 100644 index 000000000..183eba4f8 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/query/count/runner/RunnerPostCountByRunnerAssuredTest.java @@ -0,0 +1,60 @@ +package touch.baton.assure.runnerpost.query.count.runner; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport; +import touch.baton.assure.runnerpost.support.query.count.runner.RunnerPostCountByRunnerSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.query.controller.response.RunnerPostResponse; + +import java.time.LocalDateTime; +import java.util.List; + +import static touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport.러너_게시글_생성_요청; +import static touch.baton.assure.runnerpost.support.query.count.runner.RunnerPostCountByRunnerSupport.러너_게시글_개수_응답; + +@SuppressWarnings("NonAsciiCharacters") +class RunnerPostCountByRunnerAssuredTest extends AssuredTestConfig { + + @Test + void 로그인한_러너와_연관된_러너_게시글_개수_조회에_성공한다() { + // given + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode()); + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + 러너_게시글_생성에_성공한다(디투_액세스_토큰); + 러너_게시글_생성에_성공한다(헤나_액세스_토큰); + + // when + final RunnerPostResponse.Count 기대된_러너_게시글_개수 = 러너_게시글_개수_응답(1); + + // then + RunnerPostCountByRunnerSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(디투_액세스_토큰) + .리뷰_상태로_로그인한_러너와_연관된_러너_게시글_개수를_조회한다(ReviewStatus.NOT_STARTED) + + .서버_응답() + .로그인한_러너와_연관된_러너_게시글_개수_조회_성공을_검증한다(기대된_러너_게시글_개수); + } + + private void 러너_게시글_생성에_성공한다(final String 액세스_토큰) { + RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(액세스_토큰) + .러너_게시글_등록_요청한다( + 러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + List.of("자바", "스프링"), + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + ) + ) + + .서버_응답() + .러너_게시글_생성_성공을_검증한다(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/query/count/supporter/RunnerPostCountBySupporterAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/query/count/supporter/RunnerPostCountBySupporterAssuredTest.java new file mode 100644 index 000000000..94ee83049 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/query/count/supporter/RunnerPostCountBySupporterAssuredTest.java @@ -0,0 +1,188 @@ +package touch.baton.assure.runnerpost.query.count.supporter; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import touch.baton.assure.common.HttpStatusAndLocationHeader; +import touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport; +import touch.baton.assure.runnerpost.support.command.RunnerPostUpdateSupport; +import touch.baton.assure.runnerpost.support.query.count.supporter.RunnerPostCountBySupporterSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.query.controller.response.RunnerPostResponse; + +import java.time.LocalDateTime; +import java.util.List; + +import static touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport.러너_게시글_생성_요청; +import static touch.baton.assure.runnerpost.support.command.applicant.RunnerPostApplicantCreateSupport.러너의_서포터_선택_요청; +import static touch.baton.assure.runnerpost.support.command.applicant.RunnerPostApplicantCreateSupport.클라이언트_요청; +import static touch.baton.assure.runnerpost.support.query.count.supporter.RunnerPostCountBySupporterSupport.러너_게시글_개수_응답; + +@SuppressWarnings("NonAsciiCharacters") + class RunnerPostCountBySupporterAssuredTest extends AssuredTestConfig { + + @Test + void 로그인한_서포터가_지원했으면서_아직_시작하지_않은_러너_게시글_개수_조회에_성공한다() { + // given + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode()); + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + final String 서포터_에단_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ethanAuthCode()); + final Long 디투_러너_게시글_식별자_값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + + 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(헤나_액세스_토큰); + + 서포터가_러너_게시글에_리뷰_신청을_성공한다(서포터_에단_액세스_토큰, 디투_러너_게시글_식별자_값); + + // when + final RunnerPostResponse.Count 기대된_러너_게시글_개수 = 러너_게시글_개수_응답(1); + + // then + RunnerPostCountBySupporterSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(서포터_에단_액세스_토큰) + .리뷰_상태로_로그인한_서포터와_연관된_러너_게시글_개수를_조회한다(ReviewStatus.NOT_STARTED) + + .서버_응답() + .서포터와_연관된_러너_게시글_개수_조회_성공을_검증한다(기대된_러너_게시글_개수); + } + + @Test + void 로그인한_서포터가_리뷰_중인_러너_게시글_개수_조회에_성공한다() { + // given + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode()); + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + final String 서포터_에단_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ethanAuthCode()); + final SocialId 서포터_에단_소셜_아이디 = jwtTestManager.parseToSocialId(서포터_에단_액세스_토큰); + final Supporter 서포터_에단 = supporterRepository.getBySocialId(서포터_에단_소셜_아이디); + + final Long 디투_러너_게시글_식별자_값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + final Long 헤나_러너_게시글_식별자_값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(헤나_액세스_토큰); + + 서포터가_러너_게시글에_리뷰_신청을_성공한다(서포터_에단_액세스_토큰, 헤나_러너_게시글_식별자_값); + + 서포터가_러너_게시글에_리뷰_신청을_성공한다(서포터_에단_액세스_토큰, 디투_러너_게시글_식별자_값); + 러너가_서포터의_리뷰_신청_선택에_성공한다(서포터_에단, 디투_액세스_토큰, 디투_러너_게시글_식별자_값); + + // when + final RunnerPostResponse.Count 기대된_러너_게시글_개수 = 러너_게시글_개수_응답(1); + + // then + RunnerPostCountBySupporterSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(서포터_에단_액세스_토큰) + .리뷰_상태로_로그인한_서포터와_연관된_러너_게시글_개수를_조회한다(ReviewStatus.IN_PROGRESS) + + .서버_응답() + .서포터와_연관된_러너_게시글_개수_조회_성공을_검증한다(기대된_러너_게시글_개수); + } + + @Test + void 로그인한_서포터가_완료한_러너_게시글_개수_조회에_성공한다() { + // given + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode()); + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + final String 서포터_에단_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ethanAuthCode()); + final SocialId 서포터_에단_소셜_아이디 = jwtTestManager.parseToSocialId(서포터_에단_액세스_토큰); + final Supporter 서포터_에단 = supporterRepository.getBySocialId(서포터_에단_소셜_아이디); + + final Long 디투_러너_게시글_식별자_값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + final Long 헤나_러너_게시글_식별자_값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(헤나_액세스_토큰); + + 서포터가_러너_게시글에_리뷰_신청을_성공한다(서포터_에단_액세스_토큰, 헤나_러너_게시글_식별자_값); + + 서포터가_러너_게시글에_리뷰_신청을_성공한다(서포터_에단_액세스_토큰, 디투_러너_게시글_식별자_값); + 러너가_서포터의_리뷰_신청_선택에_성공한다(서포터_에단, 디투_액세스_토큰, 디투_러너_게시글_식별자_값); + 서포터가_러너_게시글의_리뷰를_완료로_변경하는_것을_성공한다(서포터_에단_액세스_토큰, 디투_러너_게시글_식별자_값); + + // when + final RunnerPostResponse.Count 기대된_러너_게시글_개수 = 러너_게시글_개수_응답(1); + + // then + RunnerPostCountBySupporterSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(서포터_에단_액세스_토큰) + .리뷰_상태로_로그인한_서포터와_연관된_러너_게시글_개수를_조회한다(ReviewStatus.DONE) + + .서버_응답() + .서포터와_연관된_러너_게시글_개수_조회_성공을_검증한다(기대된_러너_게시글_개수); + } + + @Test + void 타인이_서포터가_완료한_러너_게시글_개수_조회에_성공한다() { + // given + final String 러너_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode()); + final String 서포터_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ethanAuthCode()); + final SocialId 서포터_소셜_아이디 = jwtTestManager.parseToSocialId(서포터_액세스_토큰); + final Supporter 서포터 = supporterRepository.getBySocialId(서포터_소셜_아이디); + + final Long 러너_게시글_식별자_값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(러너_액세스_토큰); + + 서포터가_러너_게시글에_리뷰_신청을_성공한다(서포터_액세스_토큰, 러너_게시글_식별자_값); + 러너가_서포터의_리뷰_신청_선택에_성공한다(서포터, 러너_액세스_토큰, 러너_게시글_식별자_값); + 서포터가_러너_게시글의_리뷰를_완료로_변경하는_것을_성공한다(서포터_액세스_토큰, 러너_게시글_식별자_값); + + // when + final RunnerPostResponse.Count 기대된_러너_게시글_개수 = 러너_게시글_개수_응답(1); + + // then + RunnerPostCountBySupporterSupport + .클라이언트_요청() + .서포터_식별자_값으로_서포터가_완료한_러너_게시글_개수를_조회한다(서포터.getId()) + + .서버_응답() + .서포터와_연관된_러너_게시글_개수_조회_성공을_검증한다(기대된_러너_게시글_개수); + } + + private Long 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(final String 러너_액세스_토큰) { + return RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(러너_액세스_토큰) + .러너_게시글_등록_요청한다( + 러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + List.of("자바", "스프링"), + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + ) + ) + + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); + } + + private void 서포터가_러너_게시글에_리뷰_신청을_성공한다(final String 서포터_액세스_토큰, final Long 러너_게시글_식별자값) { + 클라이언트_요청() + .액세스_토큰으로_로그인한다(서포터_액세스_토큰) + .서포터가_러너_게시글에_리뷰를_신청한다(러너_게시글_식별자값, "안녕하세요. 서포터 헤나입니다.") + + .서버_응답() + .서포터가_러너_게시글에_리뷰_신청_성공을_검증한다(러너_게시글_식별자값); + } + + private void 러너가_서포터의_리뷰_신청_선택에_성공한다(final Supporter 서포터, final String 러너_액세스_토큰, final Long 러너_게시글_식별자값) { + RunnerPostUpdateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(러너_액세스_토큰) + .러너가_서포터를_선택한다(러너_게시글_식별자값, 러너의_서포터_선택_요청(서포터.getId())) + + .서버_응답() + .러너_게시글에_서포터가_성공적으로_선택되었는지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); + } + + private void 서포터가_러너_게시글의_리뷰를_완료로_변경하는_것을_성공한다(final String 서포터_액세스_토큰, final Long 러너_게시글_식별자값) { + RunnerPostUpdateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(서포터_액세스_토큰) + .서포터가_리뷰를_완료하고_리뷰완료_버튼을_누른다(러너_게시글_식별자값) + + .서버_응답() + .러너_게시글이_성공적으로_리뷰_완료_상태인지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/query/detail/RunnerPostInRunnerPostDetailAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/query/detail/RunnerPostInRunnerPostDetailAssuredTest.java new file mode 100644 index 000000000..02dd2eb78 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/query/detail/RunnerPostInRunnerPostDetailAssuredTest.java @@ -0,0 +1,97 @@ +package touch.baton.assure.runnerpost.query.detail; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport; +import touch.baton.assure.runnerpost.support.query.detail.RunnerPostDetailSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.query.controller.response.RunnerPostResponse; + +import java.time.LocalDateTime; +import java.util.List; + +import static touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport.러너_게시글_생성_요청; +import static touch.baton.assure.runnerpost.support.query.detail.RunnerPostDetailSupport.러너_게시글_Detail_응답; +import static touch.baton.fixture.vo.WatchedCountFixture.watchedCount; + +@SuppressWarnings("NonAsciiCharacters") +class RunnerPostInRunnerPostDetailAssuredTest extends AssuredTestConfig { + + @Test + void 러너의_게시글_식별자값으로_러너_게시글_상세_정보_조회에_성공한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.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 + RunnerPostDetailSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_식별자값으로_러너_게시글을_조회한다(헤나_러너_게시글_식별자값) + + .서버_응답() + .러너_게시글_단건_조회_성공을_검증한다(러너_게시글_detail_응답); + } + + private Long 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환에_성공한다(final String 헤나_액세스_토큰) { + return RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다( + 러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + List.of("자바", "스프링"), + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + ) + ) + + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); + } + + 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/query/page/RunnerPostPageAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/query/page/RunnerPostPageAssuredTest.java new file mode 100644 index 000000000..a5295d87a --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/query/page/RunnerPostPageAssuredTest.java @@ -0,0 +1,262 @@ +package touch.baton.assure.runnerpost.query.page; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.runnerpost.support.RunnerPostPageSupport; +import touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.common.response.PageResponse; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.query.controller.response.RunnerPostResponse; + +import java.time.LocalDateTime; +import java.util.List; + +import static touch.baton.assure.runnerpost.support.RunnerPostPageSupport.러너_게시글_Simple_응답; +import static touch.baton.assure.runnerpost.support.RunnerPostPageSupport.러너_게시글_전체_Simple_페이징_응답; +import static touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport.러너_게시글_생성_요청; + +@SuppressWarnings("NonAsciiCharacters") +class RunnerPostPageAssuredTest extends AssuredTestConfig { + + @Test + void 조건_없이_러너_게시글_첫_페이지_조회에_성공한다() { + // given + final String 액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final Long 러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(액세스_토큰); + + final RunnerPost 러너_게시글 = runnerPostRepository.getByRunnerPostId(러너_게시글_식별자값); + final long 서포터_지원자_수 = runnerPostRepository.countApplicantByRunnerPostId(러너_게시글_식별자값); + final int 페이지_크기 = 10; + + // when, then + final RunnerPostResponse.Simple 기대된_러너_게시글_Simple_응답 = 러너_게시글_Simple_응답( + 러너_게시글, + 0, + 서포터_지원자_수, + ReviewStatus.NOT_STARTED, + List.of("자바", "스프링") + ); + final PageResponse.PageInfo 기대된_페이징_정보 = PageResponse.PageInfo.last(); + final PageResponse 기대된_러너_게시글_전체_Simple_페이징_응답 = 러너_게시글_전체_Simple_페이징_응답( + List.of(기대된_러너_게시글_Simple_응답), + 기대된_페이징_정보 + ); + + RunnerPostPageSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(액세스_토큰) + .조건_없이_러너_게시글_첫_페이지를_조회한다(페이지_크기) + + .서버_응답() + .리뷰_상태를_조건으로_러너_게시글_페이징_조회_성공을_검증한다( + 기대된_러너_게시글_전체_Simple_페이징_응답 + ); + } + + @Test + void 조건_없이_러너_게시글_중간_페이지_조회에_성공한다() { + // given + final String 액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode()); + + final Long 다음_페이지_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(액세스_토큰); + final Long 이전_페이지_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(액세스_토큰); + + final RunnerPost 다음_페이지_러너_게시글 = runnerPostRepository.getByRunnerPostId(다음_페이지_러너_게시글_식별자값); + final long 다음_페이지_게시글_서포터_지원자_수 = runnerPostRepository.countApplicantByRunnerPostId(다음_페이지_러너_게시글_식별자값); + final int 페이지_크기 = 10; + + // when, then + final RunnerPostResponse.Simple 기대된_러너_게시글_Simple_응답 = 러너_게시글_Simple_응답( + 다음_페이지_러너_게시글, + 0, + 다음_페이지_게시글_서포터_지원자_수, + ReviewStatus.NOT_STARTED, + List.of("자바", "스프링") + ); + final PageResponse.PageInfo 기대된_페이징_정보 = PageResponse.PageInfo.last(); + final PageResponse 기대된_러너_게시글_전체_Simple_페이징_응답 = 러너_게시글_전체_Simple_페이징_응답( + List.of(기대된_러너_게시글_Simple_응답), + 기대된_페이징_정보 + ); + + RunnerPostPageSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(액세스_토큰) + .조건_없이_러너_게시글_중간_페이지를_조회한다(이전_페이지_러너_게시글_식별자값, 페이지_크기) + + .서버_응답() + .리뷰_상태를_조건으로_러너_게시글_페이징_조회_성공을_검증한다( + 기대된_러너_게시글_전체_Simple_페이징_응답 + ); + } + + @Test + void 리뷰_상태를_조건으로_러너_게시글_첫_페이지_조회에_성공한다() { + // given + final String 액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final Long 러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(액세스_토큰); + + final RunnerPost 러너_게시글 = runnerPostRepository.getByRunnerPostId(러너_게시글_식별자값); + final long 서포터_지원자_수 = runnerPostRepository.countApplicantByRunnerPostId(러너_게시글_식별자값); + final int 페이지_크기 = 10; + + // when, then + final RunnerPostResponse.Simple 기대된_러너_게시글_Simple_응답 = 러너_게시글_Simple_응답( + 러너_게시글, + 0, + 서포터_지원자_수, + ReviewStatus.NOT_STARTED, + List.of("자바", "스프링") + ); + final PageResponse.PageInfo 기대된_페이징_정보 = PageResponse.PageInfo.last(); + final PageResponse 기대된_러너_게시글_전체_Simple_페이징_응답 = 러너_게시글_전체_Simple_페이징_응답( + List.of(기대된_러너_게시글_Simple_응답), + 기대된_페이징_정보 + ); + + RunnerPostPageSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(액세스_토큰) + .리뷰_상태로_러너_게시글_첫_페이지를_조회한다(페이지_크기, ReviewStatus.NOT_STARTED) + + .서버_응답() + .리뷰_상태를_조건으로_러너_게시글_페이징_조회_성공을_검증한다( + 기대된_러너_게시글_전체_Simple_페이징_응답 + ); + } + + @Test + void 리뷰_상태를_조건으로_러너_게시글_중간_페이지_조회에_성공한다() { + // given + final String 액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode()); + + final Long 다음_페이지_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(액세스_토큰); + final Long 이전_페이지_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(액세스_토큰); + + final RunnerPost 다음_페이지_러너_게시글 = runnerPostRepository.getByRunnerPostId(다음_페이지_러너_게시글_식별자값); + final long 다음_페이지_게시글_서포터_지원자_수 = runnerPostRepository.countApplicantByRunnerPostId(다음_페이지_러너_게시글_식별자값); + final int 페이지_크기 = 10; + + // when, then + final RunnerPostResponse.Simple 기대된_러너_게시글_Simple_응답 = 러너_게시글_Simple_응답( + 다음_페이지_러너_게시글, + 0, + 다음_페이지_게시글_서포터_지원자_수, + ReviewStatus.NOT_STARTED, + List.of("자바", "스프링") + ); + final PageResponse.PageInfo 기대된_페이징_정보 = PageResponse.PageInfo.last(); + final PageResponse 기대된_러너_게시글_전체_Simple_페이징_응답 = 러너_게시글_전체_Simple_페이징_응답( + List.of(기대된_러너_게시글_Simple_응답), + 기대된_페이징_정보 + ); + + RunnerPostPageSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(액세스_토큰) + .리뷰_상태로_러너_게시글_중간_페이지를_조회한다(이전_페이지_러너_게시글_식별자값, 페이지_크기, ReviewStatus.NOT_STARTED) + + .서버_응답() + .리뷰_상태를_조건으로_러너_게시글_페이징_조회_성공을_검증한다( + 기대된_러너_게시글_전체_Simple_페이징_응답 + ); + } + + @Test + void 태그_이름과_리뷰_상태를_조건으로_러너_게시글_첫_페이지_조회에_성공한다() { + // given + final String 액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final Long 러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(액세스_토큰); + + final RunnerPost 러너_게시글 = runnerPostRepository.getByRunnerPostId(러너_게시글_식별자값); + final long 서포터_지원자_수 = runnerPostRepository.countApplicantByRunnerPostId(러너_게시글_식별자값); + final int 페이지_크기 = 10; + + // when, then + final RunnerPostResponse.Simple 기대된_러너_게시글_Simple_응답 = 러너_게시글_Simple_응답( + 러너_게시글, + 0, + 서포터_지원자_수, + ReviewStatus.NOT_STARTED, + List.of("자바", "스프링") + ); + final PageResponse.PageInfo 기대된_페이징_정보 = PageResponse.PageInfo.last(); + final PageResponse 기대된_러너_게시글_전체_Simple_페이징_응답 = 러너_게시글_전체_Simple_페이징_응답( + List.of(기대된_러너_게시글_Simple_응답), + 기대된_페이징_정보 + ); + + RunnerPostPageSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(액세스_토큰) + .태그_이름과_리뷰_상태를_조건으로_러너_게시글_첫_페이지를_조회한다(페이지_크기, "자바", ReviewStatus.NOT_STARTED) + + .서버_응답() + .태그_이름과_리뷰_상태를_조건으로_러너_게시글_페이징_조회_성공을_검증한다( + 기대된_러너_게시글_전체_Simple_페이징_응답 + ); + } + + @Test + void 태그_이름과_리뷰_상태를_조건으로_러너_게시글_중간_페이지_조회에_성공한다() { + // given + final String 액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode()); + + final Long 다음_페이지_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(액세스_토큰); + final Long 이전_페이지_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(액세스_토큰); + + final RunnerPost 현재_페이지_러너_게시글 = runnerPostRepository.getByRunnerPostId(다음_페이지_러너_게시글_식별자값); + final long 현재_페이지_게시글_서포터_지원자_수 = runnerPostRepository.countApplicantByRunnerPostId(다음_페이지_러너_게시글_식별자값); + final int 페이지_크기 = 10; + + // when, then + final RunnerPostResponse.Simple 기대된_러너_게시글_Simple_응답 = 러너_게시글_Simple_응답( + 현재_페이지_러너_게시글, + 0, + 현재_페이지_게시글_서포터_지원자_수, + ReviewStatus.NOT_STARTED, + List.of("자바", "스프링") + ); + final PageResponse.PageInfo 기대된_페이징_정보 = PageResponse.PageInfo.last(); + final PageResponse 기대된_러너_게시글_전체_Simple_페이징_응답 = 러너_게시글_전체_Simple_페이징_응답( + List.of(기대된_러너_게시글_Simple_응답), + 기대된_페이징_정보 + ); + + RunnerPostPageSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(액세스_토큰) + .태그_이름과_리뷰_상태를_조건으로_러너_게시글_중간_페이지를_조회한다(이전_페이지_러너_게시글_식별자값, 페이지_크기, "자바", ReviewStatus.NOT_STARTED) + + .서버_응답() + .태그_이름과_리뷰_상태를_조건으로_러너_게시글_페이징_조회_성공을_검증한다( + 기대된_러너_게시글_전체_Simple_페이징_응답 + ); + } + + private Long 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(final String 헤나_액세스_토큰) { + return RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다( + 러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + List.of("자바", "스프링"), + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + ) + ) + + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/query/page/runner/RunnerPostPageRunnerAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/query/page/runner/RunnerPostPageRunnerAssuredTest.java new file mode 100644 index 000000000..683c98eb5 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/query/page/runner/RunnerPostPageRunnerAssuredTest.java @@ -0,0 +1,116 @@ +package touch.baton.assure.runnerpost.query.page.runner; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport; +import touch.baton.assure.runnerpost.support.query.page.runner.RunnerPostPageRunnerSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.common.response.PageResponse; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.query.controller.response.RunnerPostResponse; + +import java.time.LocalDateTime; +import java.util.List; + +import static touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport.러너_게시글_생성_요청; +import static touch.baton.assure.runnerpost.support.query.page.runner.RunnerPostPageRunnerSupport.러너와_연관된_러너_게시글_응답; +import static touch.baton.assure.runnerpost.support.query.page.runner.RunnerPostPageRunnerSupport.러너와_연관된_러너_게시글_페이징_응답; + +@SuppressWarnings("NonAsciiCharacters") +class RunnerPostPageRunnerAssuredTest extends AssuredTestConfig { + + @Test + void 러너와_연관된_러너_게시글_첫_페이지_조회에_성공한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + final Long 헤나_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(헤나_액세스_토큰); + + final RunnerPost 헤나_러너_게시글 = runnerPostRepository.getByRunnerPostId(헤나_러너_게시글_식별자값); + final Long 헤나_러너_게시글에_지원한_서포터_수 = supporterRunnerPostRepository.getApplicantCountByRunnerPostId(헤나_러너_게시글_식별자값); + final int 페이지_크기 = 10; + + // when, then + final RunnerPostResponse.SimpleByRunner 기대된_러너_게시글_Simple_응답 = 러너와_연관된_러너_게시글_응답( + 헤나_러너_게시글, + null, + 0, + 헤나_러너_게시글에_지원한_서포터_수, + ReviewStatus.NOT_STARTED, + List.of("자바", "스프링") + ); + final PageResponse.PageInfo 기대된_페이징_정보 = PageResponse.PageInfo.last(); + final PageResponse 기대된_러너_게시글_페이징_응답 = 러너와_연관된_러너_게시글_페이징_응답( + List.of(기대된_러너_게시글_Simple_응답), + 기대된_페이징_정보 + ); + + RunnerPostPageRunnerSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너와_연관된_러너_게시글_첫_페이지를_조회한다(ReviewStatus.NOT_STARTED, 페이지_크기) + + .서버_응답() + .러너와_연관된_러너_게시글_페이징_조회_성공을_검증한다( + 기대된_러너_게시글_페이징_응답 + ); + } + + @Test + void 러너와_연관된_러너_게시글_중간_페이지_조회에_성공한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + final Long 헤나_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(헤나_액세스_토큰); + final Long 이전_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(헤나_액세스_토큰); + + final RunnerPost 헤나_러너_게시글 = runnerPostRepository.getByRunnerPostId(헤나_러너_게시글_식별자값); + final Long 헤나_러너_게시글에_지원한_서포터_수 = supporterRunnerPostRepository.getApplicantCountByRunnerPostId(헤나_러너_게시글_식별자값); + final int 페이지_크기 = 10; + + // when, then + final RunnerPostResponse.SimpleByRunner 기대된_러너_게시글_Simple_응답 = 러너와_연관된_러너_게시글_응답( + 헤나_러너_게시글, + null, + 0, + 헤나_러너_게시글에_지원한_서포터_수, + ReviewStatus.NOT_STARTED, + List.of("자바", "스프링") + ); + final PageResponse.PageInfo 기대된_페이징_정보 = PageResponse.PageInfo.last(); + final PageResponse 기대된_러너_게시글_페이징_응답 = 러너와_연관된_러너_게시글_페이징_응답( + List.of(기대된_러너_게시글_Simple_응답), + 기대된_페이징_정보 + ); + + RunnerPostPageRunnerSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너와_연관된_러너_게시글_중간_페이지를_조회한다(이전_러너_게시글_식별자값, ReviewStatus.NOT_STARTED, 페이지_크기) + + .서버_응답() + .러너와_연관된_러너_게시글_페이징_조회_성공을_검증한다( + 기대된_러너_게시글_페이징_응답 + ); + } + + private Long 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(final String 헤나_액세스_토큰) { + return RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .러너_게시글_등록_요청한다( + 러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + List.of("자바", "스프링"), + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + ) + ) + + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/query/page/supporter/RunnerPostPageSupporterAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/query/page/supporter/RunnerPostPageSupporterAssuredTest.java new file mode 100644 index 000000000..d3e7df49c --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/query/page/supporter/RunnerPostPageSupporterAssuredTest.java @@ -0,0 +1,253 @@ +package touch.baton.assure.runnerpost.query.page.supporter; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import touch.baton.assure.common.HttpStatusAndLocationHeader; +import touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport; +import touch.baton.assure.runnerpost.support.command.RunnerPostUpdateSupport; +import touch.baton.assure.runnerpost.support.query.page.supporter.RunnerPostPageSupporterSupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.common.response.PageResponse; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.query.controller.response.RunnerPostResponse; + +import java.time.LocalDateTime; +import java.util.List; + +import static touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport.러너_게시글_생성_요청; +import static touch.baton.assure.runnerpost.support.command.applicant.RunnerPostApplicantCreateSupport.러너의_서포터_선택_요청; +import static touch.baton.assure.runnerpost.support.command.applicant.RunnerPostApplicantCreateSupport.클라이언트_요청; +import static touch.baton.assure.runnerpost.support.query.page.supporter.RunnerPostPageSupporterSupport.서포터와_연관된_러너_게시글_응답; +import static touch.baton.assure.runnerpost.support.query.page.supporter.RunnerPostPageSupporterSupport.서포터와_연관된_러너_게시글_페이징_응답; + +@SuppressWarnings("NonAsciiCharacters") +class RunnerPostPageSupporterAssuredTest extends AssuredTestConfig { + + @Test + void 로그인된_서포터는_서포터와_연관된_대기중인_러너_게시글_첫_페이지_조회에_성공한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + + final RunnerPost 리뷰_신청을_대기중인_디투_러너_게시글 = runnerPostRepository.getByRunnerPostId(디투_러너_게시글_식별자값); + final long 디투_러너_게시글에_지원한_서포터_수 = runnerPostRepository.countApplicantByRunnerPostId(디투_러너_게시글_식별자값); + + final RunnerPostResponse.Simple 서포터가_리뷰를_지원한_대기중인_러너_게시글_응답 = 서포터와_연관된_러너_게시글_응답( + 리뷰_신청을_대기중인_디투_러너_게시글, + List.of("자바", "스프링"), + 0, + 디투_러너_게시글에_지원한_서포터_수, + ReviewStatus.NOT_STARTED + ); + final int 페이지_크기 = 10; + final PageResponse 서포터가_리뷰를_지원한_대기중인_러너_게시글_페이징_응답 = 서포터와_연관된_러너_게시글_페이징_응답( + List.of(서포터가_리뷰를_지원한_대기중인_러너_게시글_응답), + PageResponse.PageInfo.last() + ); + + // then + RunnerPostPageSupporterSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .로그인한_서포터의_러너_게시글_첫_페이지를_조회한다(ReviewStatus.NOT_STARTED, 페이지_크기) + + .서버_응답() + .서포터와_연관된_러너_게시글_페이징_조회_성공을_검증한다( + 서포터가_리뷰를_지원한_대기중인_러너_게시글_페이징_응답 + ); + } + + @Test + void 로그인된_서포터는_서포터와_연관된_진행중인_러너_게시글_중간_페이지_조회에_성공한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Supporter 서포터_헤나 = supporterRepository.getBySocialId(헤나_소셜_아이디); + + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + final Long 이전_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + 러너가_서포터의_리뷰_신청_선택에_성공한다(서포터_헤나, 디투_액세스_토큰, 디투_러너_게시글_식별자값); + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 이전_러너_게시글_식별자값); + 러너가_서포터의_리뷰_신청_선택에_성공한다(서포터_헤나, 디투_액세스_토큰, 이전_러너_게시글_식별자값); + + final RunnerPost 리뷰_진행중인_디투_러너_게시글 = runnerPostRepository.getByRunnerPostId(디투_러너_게시글_식별자값); + final long 디투_러너_게시글에_지원한_서포터_수 = runnerPostRepository.countApplicantByRunnerPostId(디투_러너_게시글_식별자값); + + final RunnerPostResponse.Simple 서포터가_리뷰를_지원한_진행중인_러너_게시글_응답 = 서포터와_연관된_러너_게시글_응답( + 리뷰_진행중인_디투_러너_게시글, + List.of("자바", "스프링"), + 0, + 디투_러너_게시글에_지원한_서포터_수, + ReviewStatus.IN_PROGRESS + ); + final int 페이지_크기 = 10; + final PageResponse 서포터가_리뷰를_지원한_진행중인_러너_게시글_페이징_응답 = 서포터와_연관된_러너_게시글_페이징_응답( + List.of(서포터가_리뷰를_지원한_진행중인_러너_게시글_응답), + PageResponse.PageInfo.last() + ); + + // then + RunnerPostPageSupporterSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .로그인한_서포터의_러너_게시글_중간_페이지를_조회한다(이전_러너_게시글_식별자값, ReviewStatus.IN_PROGRESS, 페이지_크기) + + .서버_응답() + .서포터와_연관된_러너_게시글_페이징_조회_성공을_검증한다( + 서포터가_리뷰를_지원한_진행중인_러너_게시글_페이징_응답 + ); + } + + @Test + void 서포터가_리뷰_완료한_러너_게시글_첫_페이지_조회에_성공한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Supporter 서포터_헤나 = supporterRepository.getBySocialId(헤나_소셜_아이디); + + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + 러너가_서포터의_리뷰_신청_선택에_성공한다(서포터_헤나, 디투_액세스_토큰, 디투_러너_게시글_식별자값); + 서포터가_러너_게시글의_리뷰를_완료로_변경하는_것을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + + final RunnerPost 리뷰_신청을_대기중인_디투_러너_게시글 = runnerPostRepository.getByRunnerPostId(디투_러너_게시글_식별자값); + final long 디투_러너_게시글에_지원한_서포터_수 = runnerPostRepository.countApplicantByRunnerPostId(디투_러너_게시글_식별자값); + + final RunnerPostResponse.Simple 서포터가_리뷰를_완료한_러너_게시글_응답 = 서포터와_연관된_러너_게시글_응답( + 리뷰_신청을_대기중인_디투_러너_게시글, + List.of("자바", "스프링"), + 0, + 디투_러너_게시글에_지원한_서포터_수, + ReviewStatus.DONE + ); + final int 페이지_크기 = 10; + final PageResponse 서포터가_리뷰를_완료한_러너_게시글_페이징_응답 = 서포터와_연관된_러너_게시글_페이징_응답( + List.of(서포터가_리뷰를_완료한_러너_게시글_응답), + PageResponse.PageInfo.last() + ); + + // then + RunnerPostPageSupporterSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .서포터와_연관된_러너_게시글_첫_페이지를_조회한다(서포터_헤나.getId(), ReviewStatus.DONE, 페이지_크기) + + .서버_응답() + .서포터와_연관된_러너_게시글_페이징_조회_성공을_검증한다( + 서포터가_리뷰를_완료한_러너_게시글_페이징_응답 + ); + } + + @Test + void 서포터가_리뷰_완료한_러너_게시글_중간_페이지_조회에_성공한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + final SocialId 헤나_소셜_아이디 = jwtTestManager.parseToSocialId(헤나_액세스_토큰); + final Supporter 서포터_헤나 = supporterRepository.getBySocialId(헤나_소셜_아이디); + + final String 디투_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.ditooAuthCode()); + final Long 디투_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + final Long 이전_러너_게시글_식별자값 = 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(디투_액세스_토큰); + + // when + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + 러너가_서포터의_리뷰_신청_선택에_성공한다(서포터_헤나, 디투_액세스_토큰, 디투_러너_게시글_식별자값); + 서포터가_러너_게시글의_리뷰를_완료로_변경하는_것을_성공한다(헤나_액세스_토큰, 디투_러너_게시글_식별자값); + 서포터가_러너_게시글에_리뷰_신청을_성공한다(헤나_액세스_토큰, 이전_러너_게시글_식별자값); + 러너가_서포터의_리뷰_신청_선택에_성공한다(서포터_헤나, 디투_액세스_토큰, 이전_러너_게시글_식별자값); + 서포터가_러너_게시글의_리뷰를_완료로_변경하는_것을_성공한다(헤나_액세스_토큰, 이전_러너_게시글_식별자값); + + final RunnerPost 리뷰가_완료된_디투_러너_게시글 = runnerPostRepository.getByRunnerPostId(디투_러너_게시글_식별자값); + final long 디투_러너_게시글에_지원한_서포터_수 = runnerPostRepository.countApplicantByRunnerPostId(디투_러너_게시글_식별자값); + + final RunnerPostResponse.Simple 서포터가_리뷰를_완료한_러너_게시글_응답 = 서포터와_연관된_러너_게시글_응답( + 리뷰가_완료된_디투_러너_게시글, + List.of("자바", "스프링"), + 0, + 디투_러너_게시글에_지원한_서포터_수, + ReviewStatus.DONE + ); + final int 페이지_크기 = 10; + final PageResponse 서포터가_리뷰를_완료한_러너_게시글_페이징_응답 = 서포터와_연관된_러너_게시글_페이징_응답( + List.of(서포터가_리뷰를_완료한_러너_게시글_응답), + PageResponse.PageInfo.last() + ); + + // then + RunnerPostPageSupporterSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .서포터와_연관된_러너_게시글_중간_페이지를_조회한다(이전_러너_게시글_식별자값, 서포터_헤나.getId(), ReviewStatus.DONE, 페이지_크기) + + .서버_응답() + .서포터와_연관된_러너_게시글_페이징_조회_성공을_검증한다( + 서포터가_리뷰를_완료한_러너_게시글_페이징_응답 + ); + } + + private void 서포터가_러너_게시글에_리뷰_신청을_성공한다(final String 서포터_액세스_토큰, final Long 러너_게시글_식별자값) { + 클라이언트_요청() + .액세스_토큰으로_로그인한다(서포터_액세스_토큰) + .서포터가_러너_게시글에_리뷰를_신청한다(러너_게시글_식별자값, "안녕하세요. 서포터 헤나입니다.") + + .서버_응답() + .서포터가_러너_게시글에_리뷰_신청_성공을_검증한다(러너_게시글_식별자값); + } + + private void 러너가_서포터의_리뷰_신청_선택에_성공한다(final Supporter 서포터_헤나, final String 디투_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + RunnerPostUpdateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(디투_액세스_토큰) + .러너가_서포터를_선택한다(디투_러너_게시글_식별자값, 러너의_서포터_선택_요청(서포터_헤나.getId())) + + .서버_응답() + .러너_게시글에_서포터가_성공적으로_선택되었는지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); + } + + private void 서포터가_러너_게시글의_리뷰를_완료로_변경하는_것을_성공한다(final String 헤나_액세스_토큰, final Long 디투_러너_게시글_식별자값) { + RunnerPostUpdateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(헤나_액세스_토큰) + .서포터가_리뷰를_완료하고_리뷰완료_버튼을_누른다(디투_러너_게시글_식별자값) + + .서버_응답() + .러너_게시글이_성공적으로_리뷰_완료_상태인지_확인한다(new HttpStatusAndLocationHeader(HttpStatus.NO_CONTENT, "/api/v1/posts/runner")); + } + + private Long 러너_게시글_생성을_성공하고_러너_게시글_식별자값을_반환한다(final String 러너_액세스_토큰) { + return RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(러너_액세스_토큰) + .러너_게시글_등록_요청한다( + 러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + List.of("자바", "스프링"), + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + ) + ) + + .서버_응답() + .러너_게시글_생성_성공을_검증한다() + .생성한_러너_게시글의_식별자값을_반환한다(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/RunnerPostPageSupport.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/RunnerPostPageSupport.java new file mode 100644 index 000000000..6582f8dcb --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/RunnerPostPageSupport.java @@ -0,0 +1,179 @@ +package touch.baton.assure.runnerpost.support; + +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.common.response.PageResponse; +import touch.baton.domain.member.query.controller.response.RunnerResponse; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.query.controller.response.RunnerPostResponse; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +public class RunnerPostPageSupport { + private RunnerPostPageSupport() { + } + + public static RunnerPostPageBuilder 클라이언트_요청() { + return new RunnerPostPageBuilder(); + } + + 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 List 러너_게시글_목록, + final PageResponse.PageInfo 페이징_정보 + ) { + return new PageResponse<>(러너_게시글_목록, 페이징_정보); + } + + public static class RunnerPostPageBuilder { + + + private ExtractableResponse response; + + private String accessToken; + + public RunnerPostPageBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; + return this; + } + + public RunnerPostPageBuilder 조건_없이_러너_게시글_첫_페이지를_조회한다(final int 페이지_크기) { + final Map queryParams = Map.of( + "limit", 페이지_크기 + ); + + response = AssuredSupport.get("/api/v1/posts/runner", new QueryParams(queryParams)); + return this; + } + + public RunnerPostPageBuilder 조건_없이_러너_게시글_중간_페이지를_조회한다(final Long 이전_페이지_마지막_게시글_식별자값, final int 페이지_크기) { + final Map queryParams = Map.of( + "cursor", 이전_페이지_마지막_게시글_식별자값, + "limit", 페이지_크기 + ); + + response = AssuredSupport.get("/api/v1/posts/runner", new QueryParams(queryParams)); + return this; + } + + public RunnerPostPageBuilder 리뷰_상태로_러너_게시글_첫_페이지를_조회한다(final int 페이지_크기, final ReviewStatus 리뷰_상태) { + final Map queryParams = Map.of( + "limit", 페이지_크기, + "reviewStatus", 리뷰_상태 + ); + + response = AssuredSupport.get("/api/v1/posts/runner", new QueryParams(queryParams)); + return this; + } + + public RunnerPostPageBuilder 리뷰_상태로_러너_게시글_중간_페이지를_조회한다(final Long 이전_페이지_마지막_게시글_식별자값, + final int 페이지_크기, + final ReviewStatus 리뷰_상태 + ) { + final Map queryParams = Map.of( + "cursor", 이전_페이지_마지막_게시글_식별자값, + "limit", 페이지_크기, + "reviewStatus", 리뷰_상태 + ); + + response = AssuredSupport.get("/api/v1/posts/runner", new QueryParams(queryParams)); + return this; + } + + public RunnerPostPageBuilder 태그_이름과_리뷰_상태를_조건으로_러너_게시글_첫_페이지를_조회한다(final int 페이지_크기, + final String 태그_이름, + final ReviewStatus 리뷰_상태 + ) { + final Map queryParams = Map.of( + "limit", 페이지_크기, + "tagName", 태그_이름, + "reviewStatus", 리뷰_상태 + ); + + response = AssuredSupport.get("/api/v1/posts/runner", new QueryParams(queryParams)); + return this; + } + + public RunnerPostPageBuilder 태그_이름과_리뷰_상태를_조건으로_러너_게시글_중간_페이지를_조회한다(final Long 이전_페이지_마지막_게시글_식별자값, + final int 페이지_크기, + final String 태그_이름, + final ReviewStatus 리뷰_상태 + ) { + final Map queryParams = Map.of( + "cursor", 이전_페이지_마지막_게시글_식별자값, + "limit", 페이지_크기, + "tagName", 태그_이름, + "reviewStatus", 리뷰_상태 + ); + + response = AssuredSupport.get("/api/v1/posts/runner", new QueryParams(queryParams)); + return this; + } + + public RunnerPostPageResponseBuilder 서버_응답() { + return new RunnerPostPageResponseBuilder(response); + } + + } + + public static class RunnerPostPageResponseBuilder { + + private final ExtractableResponse response; + + public RunnerPostPageResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + + 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).isEqualTo(러너_게시글_페이징_응답); + } + ); + } + + 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).isEqualTo(러너_게시글_페이징_응답); + } + ); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/command/RunnerPostCreateSupport.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/command/RunnerPostCreateSupport.java new file mode 100644 index 000000000..9f963615d --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/command/RunnerPostCreateSupport.java @@ -0,0 +1,99 @@ +package touch.baton.assure.runnerpost.support.command; + +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.domain.common.response.ErrorResponse; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostCreateRequest; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.springframework.http.HttpHeaders.LOCATION; + +@SuppressWarnings("NonAsciiCharacters") +public class RunnerPostCreateSupport { + + private RunnerPostCreateSupport() { + } + + public static RunnerPostCreateBuilder 클라이언트_요청() { + return new RunnerPostCreateBuilder(); + } + + public static RunnerPostCreateRequest 러너_게시글_생성_요청(final String 러너_게시글_제목, + final List 태그_목록, + final String 풀_리퀘스트, + final LocalDateTime 마감기한, + final String 구현_내용, + final String 궁금한_내용, + final String 참고_사항 + ) { + return new RunnerPostCreateRequest(러너_게시글_제목, 태그_목록, 풀_리퀘스트, 마감기한, 구현_내용, 궁금한_내용, 참고_사항); + } + + public static class RunnerPostCreateBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public RunnerPostCreateBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; + return this; + } + + public RunnerPostCreateBuilder 러너_게시글_등록_요청한다(final RunnerPostCreateRequest 게시글_생성_요청) { + response = AssuredSupport.post("/api/v1/posts/runner", accessToken, 게시글_생성_요청); + return this; + } + + public RunnerPostCreateResponseBuilder 서버_응답() { + return new RunnerPostCreateResponseBuilder(response); + } + } + + public static class RunnerPostCreateResponseBuilder { + + private final ExtractableResponse response; + + public RunnerPostCreateResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + + public RunnerPostCreateResponseBuilder 러너_게시글_생성_성공을_검증한다() { + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + softly.assertThat(response.header(LOCATION)).startsWith("/api/v1/posts/runner/"); + }); + + return this; + } + + public Long 생성한_러너_게시글의_식별자값을_반환한다() { + final String savedRunnerPostId = this.response.header(LOCATION).replaceFirst("/api/v1/posts/runner/", ""); + + return Long.parseLong(savedRunnerPostId); + } + + public void 러너_게시글_등록_성공을_검증한다(final HttpStatusAndLocationHeader httpStatusAndLocationHeader) { + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(httpStatusAndLocationHeader.getHttpStatus().value()); + softly.assertThat(response.header(LOCATION)).contains(httpStatusAndLocationHeader.getLocation()); + } + ); + } + + public void 러너_게시글_등록_실패를_검증한다(final ErrorResponse 예상_에러_응답) { + final ErrorResponse 실제_에러_응답 = response.as(ErrorResponse.class); + + assertSoftly(softly -> { + softly.assertThat(실제_에러_응답.errorCode()).isEqualTo(예상_에러_응답.errorCode()); + softly.assertThat(실제_에러_응답.message()).isEqualTo(예상_에러_응답.message()); + }); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/command/RunnerPostDeleteSupport.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/command/RunnerPostDeleteSupport.java new file mode 100644 index 000000000..62309a942 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/command/RunnerPostDeleteSupport.java @@ -0,0 +1,60 @@ +package touch.baton.assure.runnerpost.support.command; + +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 java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("NonAsciiCharacters") +public class RunnerPostDeleteSupport { + + private RunnerPostDeleteSupport() { + } + + public static RunnerPostDeleteBuilder 클라이언트_요청() { + return new RunnerPostDeleteBuilder(); + } + + public static class RunnerPostDeleteBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public RunnerPostDeleteBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; + return this; + } + + public RunnerPostDeleteBuilder 러너_게시글_식별자값으로_러너_게시글을_삭제한다(final Long 러너_게시글_식별자값) { + response = AssuredSupport.delete("/api/v1/posts/runner/{runnerPostId}", accessToken, new PathParams(Map.of("runnerPostId", 러너_게시글_식별자값))); + return this; + } + + public RunnerPostDeleteResponseBuilder 서버_응답() { + return new RunnerPostDeleteResponseBuilder(response); + } + } + + public static class RunnerPostDeleteResponseBuilder { + + private final ExtractableResponse response; + + public RunnerPostDeleteResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + + public void 러너_게시글_삭제_성공을_검증한다(final HttpStatus HTTP_STATUS) { + assertThat(response.statusCode()).isEqualTo(HTTP_STATUS.value()); + } + + 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/support/command/RunnerPostUpdateSupport.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/command/RunnerPostUpdateSupport.java new file mode 100644 index 000000000..a3605bf13 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/command/RunnerPostUpdateSupport.java @@ -0,0 +1,81 @@ +package touch.baton.assure.runnerpost.support.command; + +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.assure.common.PathParams; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostUpdateRequest; + +import java.util.Map; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.springframework.http.HttpHeaders.LOCATION; + +@SuppressWarnings("NonAsciiCharacters") +public class RunnerPostUpdateSupport { + + private RunnerPostUpdateSupport() { + } + + public static RunnerPostUpdateBuilder 클라이언트_요청() { + return new RunnerPostUpdateBuilder(); + } + + + public static class RunnerPostUpdateBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public RunnerPostUpdateBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; + return this; + } + + public RunnerPostUpdateBuilder 서포터가_리뷰를_완료하고_리뷰완료_버튼을_누른다(final Long 게시글_식별자) { + response = AssuredSupport.patch("/api/v1/posts/runner/{runnerPostId}/done", accessToken, new PathParams(Map.of("runnerPostId", 게시글_식별자))); + return this; + } + + public RunnerPostUpdateBuilder 러너가_서포터를_선택한다(final Long 게시글_식별자값, + final RunnerPostUpdateRequest.SelectSupporter 서포터_선택_요청_정보 + ) { + response = AssuredSupport.patch("/api/v1/posts/runner/{runnerPostId}/supporters", + accessToken, + new PathParams(Map.of("runnerPostId", 게시글_식별자값)), + 서포터_선택_요청_정보 + ); + return this; + } + + public RunnerPostUpdateResponseBuilder 서버_응답() { + return new RunnerPostUpdateResponseBuilder(response); + } + } + + public static class RunnerPostUpdateResponseBuilder { + + private final ExtractableResponse response; + + public RunnerPostUpdateResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + + public void 러너_게시글이_성공적으로_리뷰_완료_상태인지_확인한다(final HttpStatusAndLocationHeader httpStatusAndLocationHeader) { + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(httpStatusAndLocationHeader.getHttpStatus().value()); + softly.assertThat(response.header(LOCATION)).contains(httpStatusAndLocationHeader.getLocation()); + } + ); + } + + public void 러너_게시글에_서포터가_성공적으로_선택되었는지_확인한다(final HttpStatusAndLocationHeader httpStatusAndLocationHeader) { + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(httpStatusAndLocationHeader.getHttpStatus().value()); + softly.assertThat(response.header(LOCATION)).contains(httpStatusAndLocationHeader.getLocation()); + }); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/command/applicant/RunnerPostApplicantCreateSupport.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/command/applicant/RunnerPostApplicantCreateSupport.java new file mode 100644 index 000000000..71c63cfbe --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/command/applicant/RunnerPostApplicantCreateSupport.java @@ -0,0 +1,78 @@ +package touch.baton.assure.runnerpost.support.command.applicant; + +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.domain.runnerpost.command.service.dto.RunnerPostApplicantCreateRequest; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostUpdateRequest; + +import java.util.Map; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.springframework.http.HttpHeaders.LOCATION; + +@SuppressWarnings("NonAsciiCharacters") +public class RunnerPostApplicantCreateSupport { + + private RunnerPostApplicantCreateSupport() { + } + + public static RunnerPostApplicantCreateBuilder 클라이언트_요청() { + return new RunnerPostApplicantCreateBuilder(); + } + + public static RunnerPostUpdateRequest.SelectSupporter 러너의_서포터_선택_요청(final Long 러너가_선택한_서포터_식별자값) { + return new RunnerPostUpdateRequest.SelectSupporter(러너가_선택한_서포터_식별자값); + } + + public static class RunnerPostApplicantCreateBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public RunnerPostApplicantCreateBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; + return this; + } + + public RunnerPostApplicantCreateBuilder 서포터가_러너_게시글에_리뷰를_신청한다(final Long 러너_게시글_식별자값, final String 리뷰_지원_메시지) { + response = AssuredSupport.post("/api/v1/posts/runner/{runnerPostId}/application", + accessToken, + new PathParams(Map.of("runnerPostId", 러너_게시글_식별자값)), + new RunnerPostApplicantCreateRequest(리뷰_지원_메시지) + ); + return this; + } + + public RunnerPostApplicantCreateResponseBuilder 서버_응답() { + return new RunnerPostApplicantCreateResponseBuilder(response); + } + } + + public static class RunnerPostApplicantCreateResponseBuilder { + + private final ExtractableResponse response; + + public RunnerPostApplicantCreateResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + + public Long 생성한_러너_게시글의_식별자값을_반환한다() { + final String savedRunnerPostId = this.response.header(LOCATION).replaceFirst("/api/v1/posts/runner/", ""); + + return Long.parseLong(savedRunnerPostId); + } + + public RunnerPostApplicantCreateResponseBuilder 서포터가_러너_게시글에_리뷰_신청_성공을_검증한다(final Long 러너_게시글_식별자값) { + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + softly.assertThat(response.header(LOCATION)).startsWith("/api/v1/posts/runner/" + 러너_게시글_식별자값); + }); + + return this; + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/command/applicant/RunnerPostApplicantDeleteAssuredSupport.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/command/applicant/RunnerPostApplicantDeleteAssuredSupport.java new file mode 100644 index 000000000..96da592fe --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/command/applicant/RunnerPostApplicantDeleteAssuredSupport.java @@ -0,0 +1,60 @@ +package touch.baton.assure.runnerpost.support.command.applicant; + +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.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 RunnerPostApplicantDeleteAssuredSupport { + + private RunnerPostApplicantDeleteAssuredSupport() { + } + + public static SupporterRunnerPostClientRequestBuilder 클라이언트_요청() { + return new SupporterRunnerPostClientRequestBuilder(); + } + + public static class SupporterRunnerPostClientRequestBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public SupporterRunnerPostClientRequestBuilder 액세스_토큰으로_로그인_한다(final String 액세스_토큰) { + accessToken = 액세스_토큰; + return this; + } + + public SupporterRunnerPostClientRequestBuilder 서포터가_리뷰_제안을_취소한다(final Long 러너_게시글_식별자값) { + response = AssuredSupport.patch("/api/v1/posts/runner/{runnerPostId}/cancelation", accessToken, new PathParams(Map.of("runnerPostId", 러너_게시글_식별자값))); + return this; + } + + public SupporterRunnerPostServerResponseBuilder 서버_응답() { + return new SupporterRunnerPostServerResponseBuilder(response); + } + } + + public static class SupporterRunnerPostServerResponseBuilder { + + private final ExtractableResponse response; + + public SupporterRunnerPostServerResponseBuilder(final ExtractableResponse 응답) { + this.response = 응답; + } + + public void 서포터의_리뷰_제안_철회를_검증한다(final HttpStatusAndLocationHeader 응답상태_및_로케이션) { + assertSoftly(softly -> { + 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/runnerpost/support/query/applicant/RunnerPostApplicantQuerySupport.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/applicant/RunnerPostApplicantQuerySupport.java new file mode 100644 index 000000000..b4719ff3d --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/applicant/RunnerPostApplicantQuerySupport.java @@ -0,0 +1,117 @@ +package touch.baton.assure.runnerpost.support.query.applicant; + +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.PathParams; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.runnerpost.query.controller.response.SupporterRunnerPostResponse; +import touch.baton.domain.runnerpost.query.controller.response.SupporterRunnerPostResponses; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.springframework.http.HttpHeaders.LOCATION; + +@SuppressWarnings("NonAsciiCharacters") +public class RunnerPostApplicantQuerySupport { + + private RunnerPostApplicantQuerySupport() { + } + + public static RunnerPostApplicantQueryBuilder 클라이언트_요청() { + return new RunnerPostApplicantQueryBuilder(); + } + + public static SupporterRunnerPostResponses.Detail 지원한_서포터_목록_응답(final List 지원한_서포터_응답_목록) { + return SupporterRunnerPostResponses.Detail.from(지원한_서포터_응답_목록); + } + + 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 class RunnerPostApplicantQueryBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public RunnerPostApplicantQueryBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; + return this; + } + + public RunnerPostApplicantQueryBuilder 러너_게시글_식별자값으로_지원한_서포터_목록을_조회한다(final Long 러너_게시글_식별자값) { + response = AssuredSupport.get( + "/api/v1/posts/runner/{runnerPostId}/supporters", + accessToken, + new PathParams(Map.of("runnerPostId", 러너_게시글_식별자값))); + return this; + } + + public RunnerPostApplicantQueryResponseBuilder 서버_응답() { + return new RunnerPostApplicantQueryResponseBuilder(response); + } + } + + public static class RunnerPostApplicantQueryResponseBuilder { + + private final ExtractableResponse response; + + public RunnerPostApplicantQueryResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + + public RunnerPostApplicantQueryResponseBuilder 러너_게시글_생성_성공을_검증한다() { + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + softly.assertThat(response.header(LOCATION)).startsWith("/api/v1/posts/runner/"); + }); + + return this; + } + + public Long 생성한_러너_게시글의_식별자값을_반환한다() { + final String savedRunnerPostId = this.response.header(LOCATION).replaceFirst("/api/v1/posts/runner/", ""); + + return Long.parseLong(savedRunnerPostId); + } + + public RunnerPostApplicantQueryResponseBuilder 서포터가_러너_게시글에_리뷰_신청_성공을_검증한다(final Long 러너_게시글_식별자값) { + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + softly.assertThat(response.header(LOCATION)).startsWith("/api/v1/posts/runner/" + 러너_게시글_식별자값); + }); + + return this; + } + + public void 지원한_서포터_목록_조회_성공을_검증한다(final SupporterRunnerPostResponses.Detail 전체_러너_게시글_페이징_응답) { + final SupporterRunnerPostResponses.Detail actual = this.response.as(new TypeRef<>() { + + }); + + assertSoftly(softly -> { + softly.assertThat(this.response.statusCode()).isEqualTo(HttpStatus.OK.value()); + softly.assertThat(actual).isEqualTo(전체_러너_게시글_페이징_응답); + } + ); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/count/runner/RunnerPostCountByRunnerSupport.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/count/runner/RunnerPostCountByRunnerSupport.java new file mode 100644 index 000000000..3f2dbaedc --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/count/runner/RunnerPostCountByRunnerSupport.java @@ -0,0 +1,68 @@ +package touch.baton.assure.runnerpost.support.query.count.runner; + +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.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.query.controller.response.RunnerPostResponse; + +import java.util.Map; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +public class RunnerPostCountByRunnerSupport { + + private RunnerPostCountByRunnerSupport() { + } + + public static RunnerPostCountByRunnerBuilder 클라이언트_요청() { + return new RunnerPostCountByRunnerBuilder(); + } + + public static RunnerPostResponse.Count 러너_게시글_개수_응답(final long 게시글_개수) { + return new RunnerPostResponse.Count(게시글_개수); + } + + public static class RunnerPostCountByRunnerBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public RunnerPostCountByRunnerBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; + return this; + } + + public RunnerPostCountByRunnerBuilder 리뷰_상태로_로그인한_러너와_연관된_러너_게시글_개수를_조회한다(final ReviewStatus 리뷰_상태) { + final Map queryParams = Map.of("reviewStatus", 리뷰_상태); + + response = AssuredSupport.get("/api/v1/posts/runner/me/runner/count", accessToken, new QueryParams(queryParams)); + return this; + } + + public RunnerPostCountByRunnerResponseBuilder 서버_응답() { + return new RunnerPostCountByRunnerResponseBuilder(response); + } + } + + public static class RunnerPostCountByRunnerResponseBuilder { + + private final ExtractableResponse response; + + public RunnerPostCountByRunnerResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + + public void 로그인한_러너와_연관된_러너_게시글_개수_조회_성공을_검증한다(final RunnerPostResponse.Count 러너_게시글_개수_응답) { + final RunnerPostResponse.Count actual = this.response.as(RunnerPostResponse.Count.class); + assertSoftly(softly -> { + softly.assertThat(this.response.statusCode()).isEqualTo(HttpStatus.OK.value()); + softly.assertThat(actual).isEqualTo(러너_게시글_개수_응답); + }); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/count/supporter/RunnerPostCountBySupporterSupport.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/count/supporter/RunnerPostCountBySupporterSupport.java new file mode 100644 index 000000000..20c4cf08b --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/count/supporter/RunnerPostCountBySupporterSupport.java @@ -0,0 +1,78 @@ +package touch.baton.assure.runnerpost.support.query.count.supporter; + +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.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.query.controller.response.RunnerPostResponse; + +import java.util.Map; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +public class RunnerPostCountBySupporterSupport { + + private RunnerPostCountBySupporterSupport() { + } + + public static RunnerPostCountBySupporterBuilder 클라이언트_요청() { + return new RunnerPostCountBySupporterBuilder(); + } + + public static RunnerPostResponse.Count 러너_게시글_개수_응답(final long 게시글_개수) { + return new RunnerPostResponse.Count(게시글_개수); + } + + public static class RunnerPostCountBySupporterBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public RunnerPostCountBySupporterBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; + return this; + } + + public RunnerPostCountBySupporterBuilder 리뷰_상태로_로그인한_서포터와_연관된_러너_게시글_개수를_조회한다(final ReviewStatus 리뷰_상태) { + final Map queryParams = Map.of("reviewStatus", 리뷰_상태); + + response = AssuredSupport.get("/api/v1/posts/runner/me/supporter/count", accessToken, new QueryParams(queryParams)); + return this; + } + + public RunnerPostCountBySupporterBuilder 서포터_식별자_값으로_서포터가_완료한_러너_게시글_개수를_조회한다(final Long 서포터_식별자_값) { + final Map queryParams = Map.of( + "reviewStatus", ReviewStatus.DONE, + "supporterId", 서포터_식별자_값 + ); + + response = AssuredSupport.get("/api/v1/posts/runner/search/count", new QueryParams(queryParams)); + return this; + } + + public RunnerPostCountBySupporterResponseBuilder 서버_응답() { + return new RunnerPostCountBySupporterResponseBuilder(response); + } + } + + public static class RunnerPostCountBySupporterResponseBuilder { + + private final ExtractableResponse response; + + public RunnerPostCountBySupporterResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + + public void 서포터와_연관된_러너_게시글_개수_조회_성공을_검증한다(final RunnerPostResponse.Count 러너_게시글_개수_응답) { + final RunnerPostResponse.Count actual = this.response.as(RunnerPostResponse.Count.class); + assertSoftly(softly -> { + softly.assertThat(this.response.statusCode()).isEqualTo(HttpStatus.OK.value()); + softly.assertThat(actual).isEqualTo(러너_게시글_개수_응답); + }); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/detail/RunnerPostDetailSupport.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/detail/RunnerPostDetailSupport.java new file mode 100644 index 000000000..a82a1c0ee --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/detail/RunnerPostDetailSupport.java @@ -0,0 +1,117 @@ +package touch.baton.assure.runnerpost.support.query.detail; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import touch.baton.assure.common.AssuredSupport; +import touch.baton.assure.common.PathParams; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.query.controller.response.RunnerResponse; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.query.controller.response.RunnerPostResponse; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +public class RunnerPostDetailSupport { + + private RunnerPostDetailSupport() { + } + + public static RunnerPostDetailBuilder 클라이언트_요청() { + return new RunnerPostDetailBuilder(); + } + + public static RunnerPostResponse.Detail 러너_게시글_Detail_응답(final Long 러너_게시글_식별자값, + final String 제목, + final String 구현_내용, + final String 궁금한_내용, + final String 참고_사항, + final String 풀_리퀘스트, + final LocalDateTime 마감기한, + final int 조회수, + final long 서포터_지원자수, + final ReviewStatus 리뷰_상태, + final boolean 주인_여부, + final boolean 서포터_지원_여부, + final Runner 러너, + final List 태그_목록 + ) { + return new RunnerPostResponse.Detail( + 러너_게시글_식별자값, + 제목, + 구현_내용, + 궁금한_내용, + 참고_사항, + 풀_리퀘스트, + 마감기한, + 조회수, + 서포터_지원자수, + 리뷰_상태, + 주인_여부, + 서포터_지원_여부, + 태그_목록, + RunnerResponse.InRunnerPostDetail.from(러너) + ); + } + + public static class RunnerPostDetailBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public RunnerPostDetailBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; + return this; + } + + public RunnerPostDetailBuilder 러너_게시글_식별자값으로_러너_게시글을_조회한다(final Long 러너_게시글_식별자값) { + response = AssuredSupport.get( + "/api/v1/posts/runner/{runnerPostId}", + accessToken, + new PathParams(Map.of("runnerPostId", 러너_게시글_식별자값))); + return this; + } + + public RunnerPostDetailResponseBuilder 서버_응답() { + return new RunnerPostDetailResponseBuilder(response); + } + } + + public static class RunnerPostDetailResponseBuilder { + + private final ExtractableResponse response; + + public void 러너_게시글_단건_조회_성공을_검증한다(final RunnerPostResponse.Detail 러너_게시글_응답) { + final RunnerPostResponse.Detail actual = this.response.as(RunnerPostResponse.Detail.class); + + assertSoftly(softly -> { + softly.assertThat(actual.runnerPostId()).isEqualTo(러너_게시글_응답.runnerPostId()); + softly.assertThat(actual.title()).isEqualTo(러너_게시글_응답.title()); + 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()); + softly.assertThat(actual.reviewStatus()).isEqualTo(러너_게시글_응답.reviewStatus()); + softly.assertThat(actual.tags()).isEqualTo(러너_게시글_응답.tags()); + softly.assertThat(actual.deadline()).isEqualToIgnoringSeconds(러너_게시글_응답.deadline()); + softly.assertThat(actual.runnerProfile().name()).isEqualTo(러너_게시글_응답.runnerProfile().name()); + softly.assertThat(actual.runnerProfile().company()).isEqualTo(러너_게시글_응답.runnerProfile().company()); + softly.assertThat(actual.runnerProfile().imageUrl()).isEqualTo(러너_게시글_응답.runnerProfile().imageUrl()); + softly.assertThat(actual.runnerProfile().runnerId()).isEqualTo(러너_게시글_응답.runnerProfile().runnerId()); + softly.assertThat(actual.watchedCount()).isEqualTo(러너_게시글_응답.watchedCount()); + softly.assertThat(actual.runnerPostId()).isEqualTo(러너_게시글_응답.runnerPostId()); + } + ); + } + public RunnerPostDetailResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/page/runner/RunnerPostPageRunnerSupport.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/page/runner/RunnerPostPageRunnerSupport.java new file mode 100644 index 000000000..9af7555e3 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/page/runner/RunnerPostPageRunnerSupport.java @@ -0,0 +1,118 @@ +package touch.baton.assure.runnerpost.support.query.page.runner; + +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.common.response.PageResponse; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.query.controller.response.RunnerPostResponse; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +public class RunnerPostPageRunnerSupport { + + private RunnerPostPageRunnerSupport() { + } + + public static RunnerPostPageRunnerBuilder 클라이언트_요청() { + return new RunnerPostPageRunnerBuilder(); + } + + + public static RunnerPostResponse.SimpleByRunner 러너와_연관된_러너_게시글_응답(final RunnerPost 러너_게시글, + final Long 지원한_서포터_식별자값, + final int 조회수, + final long 지원한_서포터_수, + final ReviewStatus 리뷰_상태, + final List 러너_게시글_태그_목록 + ) { + return new RunnerPostResponse.SimpleByRunner( + 러너_게시글.getId(), + 러너_게시글.getTitle().getValue(), + 러너_게시글.getDeadline().getValue(), + 조회수, + 지원한_서포터_수, + 리뷰_상태.name(), + 러너_게시글.getIsReviewed().getValue(), + 지원한_서포터_식별자값, + 러너_게시글_태그_목록 + ); + } + + public static PageResponse 러너와_연관된_러너_게시글_페이징_응답( + final List 러너_게시글_목록, + final PageResponse.PageInfo 페이징_정보 + ) { + return new PageResponse<>(러너_게시글_목록, 페이징_정보); + } + + public static class RunnerPostPageRunnerBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public RunnerPostPageRunnerBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; + return this; + } + + public RunnerPostPageRunnerBuilder 러너와_연관된_러너_게시글_첫_페이지를_조회한다(final ReviewStatus 리뷰_상태, final int 페이지_크기) { + final Map queryParams = Map.of( + "reviewStatus", 리뷰_상태, + "limit", 페이지_크기 + ); + + response = AssuredSupport.get("/api/v1/posts/runner/me/runner", accessToken, new QueryParams(queryParams)); + return this; + } + + public RunnerPostPageRunnerBuilder 러너와_연관된_러너_게시글_중간_페이지를_조회한다(final Long 이전_페이지_마지막_게시글_식별자값, + final ReviewStatus 리뷰_상태, + final int 페이지_크기 + ) { + final Map queryParams = Map.of( + "cursor", 이전_페이지_마지막_게시글_식별자값, + "reviewStatus", 리뷰_상태, + "limit", 페이지_크기 + ); + + response = AssuredSupport.get("/api/v1/posts/runner/me/runner", accessToken, new QueryParams(queryParams)); + return this; + } + + public RunnerPostPageRunnerResponseBuilder 서버_응답() { + return new RunnerPostPageRunnerResponseBuilder(response); + } + } + + public static class RunnerPostPageRunnerResponseBuilder { + + private final ExtractableResponse response; + + public RunnerPostPageRunnerResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + + 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()); + } + ); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/page/supporter/RunnerPostPageSupporterSupport.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/page/supporter/RunnerPostPageSupporterSupport.java new file mode 100644 index 000000000..ad7002425 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/support/query/page/supporter/RunnerPostPageSupporterSupport.java @@ -0,0 +1,146 @@ +package touch.baton.assure.runnerpost.support.query.page.supporter; + +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.common.response.PageResponse; +import touch.baton.domain.member.query.controller.response.RunnerResponse; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.query.controller.response.RunnerPostResponse; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +public class RunnerPostPageSupporterSupport { + + private RunnerPostPageSupporterSupport() { + } + + public static RunnerPostPageSupporterBuilder 클라이언트_요청() { + return new RunnerPostPageSupporterBuilder(); + } + + public static RunnerPostResponse.Simple 서포터와_연관된_러너_게시글_응답(final RunnerPost 러너_게시글, + final List 러너_게시글_태그_목록, + final int 조회수, + final long 러너_게시글에_지원한_서포터수, + final ReviewStatus 리뷰_상태 + ) { + return new RunnerPostResponse.Simple( + 러너_게시글.getId(), + 러너_게시글.getTitle().getValue(), + 러너_게시글.getDeadline().getValue(), + 조회수, + 러너_게시글에_지원한_서포터수, + 리뷰_상태.name(), + RunnerResponse.Simple.from(러너_게시글.getRunner()), + 러너_게시글_태그_목록 + ); + } + + public static PageResponse 서포터와_연관된_러너_게시글_페이징_응답( + final List 러너_게시글_목록, + final PageResponse.PageInfo 페이징_정보 + ) { + return new PageResponse<>(러너_게시글_목록, 페이징_정보); + } + + public static class RunnerPostPageSupporterBuilder { + + private ExtractableResponse response; + + private String accessToken; + + public RunnerPostPageSupporterBuilder 액세스_토큰으로_로그인한다(final String 액세스_토큰) { + this.accessToken = 액세스_토큰; + return this; + } + + public RunnerPostPageSupporterBuilder 로그인한_서포터의_러너_게시글_첫_페이지를_조회한다(final ReviewStatus 리뷰_진행_상태, + final int 페이지_크기 + ) { + final Map queryParams = Map.of( + "reviewStatus", 리뷰_진행_상태, + "limit", 페이지_크기 + ); + + response = AssuredSupport.get("/api/v1/posts/runner/me/supporter", accessToken, new QueryParams(queryParams)); + return this; + } + + public RunnerPostPageSupporterBuilder 로그인한_서포터의_러너_게시글_중간_페이지를_조회한다(final Long 이전_페이지_마지막_게시글_식별자값, + final ReviewStatus 리뷰_진행_상태, + final int 페이지_크기 + ) { + final Map queryParams = Map.of( + "cursor", 이전_페이지_마지막_게시글_식별자값, + "reviewStatus", 리뷰_진행_상태, + "limit", 페이지_크기 + ); + + response = AssuredSupport.get("/api/v1/posts/runner/me/supporter", accessToken, new QueryParams(queryParams)); + return this; + } + + public RunnerPostPageSupporterBuilder 서포터와_연관된_러너_게시글_첫_페이지를_조회한다(final Long 서포터_식별자값, + final ReviewStatus 리뷰_진행_상태, + final int 페이지_크기 + ) { + final Map queryParams = Map.of( + "supporterId", 서포터_식별자값, + "reviewStatus", 리뷰_진행_상태, + "limit", 페이지_크기 + ); + + response = AssuredSupport.get("/api/v1/posts/runner/search", new QueryParams(queryParams)); + return this; + } + + public RunnerPostPageSupporterBuilder 서포터와_연관된_러너_게시글_중간_페이지를_조회한다(final Long 이전_페이지_마지막_게시글_식별자값, + final Long 서포터_식별자값, + final ReviewStatus 리뷰_진행_상태, + final int 페이지_크기 + ) { + final Map queryParams = Map.of( + "cursor", 이전_페이지_마지막_게시글_식별자값, + "supporterId", 서포터_식별자값, + "reviewStatus", 리뷰_진행_상태, + "limit", 페이지_크기 + ); + + response = AssuredSupport.get("/api/v1/posts/runner/search", new QueryParams(queryParams)); + return this; + } + + public RunnerPostPageSupporterResponseBuilder 서버_응답() { + return new RunnerPostPageSupporterResponseBuilder(response); + } + } + + public static class RunnerPostPageSupporterResponseBuilder { + + private final ExtractableResponse response; + + public RunnerPostPageSupporterResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + + 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).isEqualTo(서포터와_연관된_러너_게시글_페이징_응답); + } + ); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/tag/query/TagReadAssuredTest.java b/backend/baton/src/test/java/touch/baton/assure/tag/query/TagReadAssuredTest.java new file mode 100644 index 000000000..e491a3c53 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/tag/query/TagReadAssuredTest.java @@ -0,0 +1,59 @@ +package touch.baton.assure.tag.query; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport; +import touch.baton.assure.tag.support.query.TagQuerySupport; +import touch.baton.config.AssuredTestConfig; +import touch.baton.config.infra.auth.oauth.authcode.FakeAuthCodes; +import touch.baton.domain.tag.command.Tag; +import touch.baton.domain.tag.command.vo.TagReducedName; +import touch.baton.domain.tag.query.controller.response.TagSearchResponses; + +import java.time.LocalDateTime; +import java.util.List; + +import static touch.baton.assure.runnerpost.support.command.RunnerPostCreateSupport.러너_게시글_생성_요청; + + +@SuppressWarnings("NonAsciiCharacters") +class TagReadAssuredTest extends AssuredTestConfig { + + @Test + void 입력된_문자열로_태그_목록_검색에_성공한다() { + // given + final String 헤나_액세스_토큰 = oauthLoginTestManager.소셜_회원가입을_진행한_후_액세스_토큰을_반환한다(FakeAuthCodes.hyenaAuthCode()); + 러너_게시글과_태그를_생성한다(헤나_액세스_토큰, List.of("java", "javascript", "script")); + + final TagReducedName 요청할_태그_이름 = TagReducedName.nullableInstance("ja"); + + // when, then + final List 검색된_태그_목록 = tagQueryRepository.findByTagReducedName(요청할_태그_이름, 10); + + TagQuerySupport + .클라이언트_요청() + .입력된_문자열로_태그_목록을_검색한다(요청할_태그_이름) + + .서버_응답() + .입력된_문자열로_태그_목록_검색_성공을_검증한다( + TagSearchResponses.Detail.from(검색된_태그_목록) + ); + } + + private void 러너_게시글과_태그를_생성한다(final String 액세스_토큰, final List 태그_목록) { + RunnerPostCreateSupport + .클라이언트_요청() + .액세스_토큰으로_로그인한다(액세스_토큰) + .러너_게시글_등록_요청한다(러너_게시글_생성_요청( + "테스트용_러너_게시글_제목", + 태그_목록, + "https://test-pull-request.com", + LocalDateTime.now().plusHours(100), + "테스트용_러너_게시글_구현_내용", + "테스트용_러너_게시글_궁금한_내용", + "테스트용_러너_게시글_참고_사항" + )) + + .서버_응답() + .러너_게시글_생성_성공을_검증한다(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/tag/support/query/TagQuerySupport.java b/backend/baton/src/test/java/touch/baton/assure/tag/support/query/TagQuerySupport.java new file mode 100644 index 000000000..115fbde5b --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/tag/support/query/TagQuerySupport.java @@ -0,0 +1,58 @@ +package touch.baton.assure.tag.support.query; + +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.tag.command.vo.TagReducedName; +import touch.baton.domain.tag.query.controller.response.TagSearchResponses; + +import java.util.Map; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +public class TagQuerySupport { + + private TagQuerySupport() { + } + + public static TagQueryBuilder 클라이언트_요청() { + return new TagQueryBuilder(); + } + + public static class TagQueryBuilder { + + private ExtractableResponse response; + + public TagQueryBuilder 입력된_문자열로_태그_목록을_검색한다(final TagReducedName 요청할_태그_이름) { + response = AssuredSupport.get("/api/v1/tags/search", new QueryParams(Map.of("tagName", 요청할_태그_이름.getValue()))); + return this; + } + + public TagQueryResponseBuilder 서버_응답() { + return new TagQueryResponseBuilder(response); + } + } + + public static class TagQueryResponseBuilder { + + private final ExtractableResponse response; + + public TagQueryResponseBuilder(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()); + }); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/common/schedule/RunnerPostDeadlineCheckSchedulerTest.java b/backend/baton/src/test/java/touch/baton/common/schedule/RunnerPostDeadlineCheckSchedulerTest.java index 1cb5b3622..546ea6272 100644 --- a/backend/baton/src/test/java/touch/baton/common/schedule/RunnerPostDeadlineCheckSchedulerTest.java +++ b/backend/baton/src/test/java/touch/baton/common/schedule/RunnerPostDeadlineCheckSchedulerTest.java @@ -6,10 +6,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; 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.vo.Deadline; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.Deadline; import touch.baton.fixture.domain.MemberFixture; import touch.baton.fixture.domain.RunnerFixture; import touch.baton.fixture.domain.RunnerPostFixture; @@ -17,7 +17,7 @@ import java.time.LocalDateTime; import static org.assertj.core.api.Assertions.assertThat; -import static touch.baton.domain.runnerpost.vo.ReviewStatus.OVERDUE; +import static touch.baton.domain.runnerpost.command.vo.ReviewStatus.OVERDUE; import static touch.baton.fixture.vo.DeadlineFixture.deadline; class RunnerPostDeadlineCheckSchedulerTest extends ServiceTestConfig { diff --git a/backend/baton/src/test/java/touch/baton/common/schedule/ScheduleRunnerPostQueryRepositoryTest.java b/backend/baton/src/test/java/touch/baton/common/schedule/ScheduleRunnerPostQueryRepositoryTest.java new file mode 100644 index 000000000..7c9d3fc0a --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/common/schedule/ScheduleRunnerPostQueryRepositoryTest.java @@ -0,0 +1,108 @@ +package touch.baton.common.schedule; + +import jakarta.persistence.EntityManager; +import org.junit.jupiter.api.BeforeEach; +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.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.Deadline; +import touch.baton.domain.runnerpost.command.vo.IsReviewed; +import touch.baton.domain.runnerpost.command.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 org.assertj.core.api.Assertions.assertThat; +import static touch.baton.domain.runnerpost.command.vo.ReviewStatus.*; +import static touch.baton.fixture.vo.DeadlineFixture.deadline; + +class ScheduleRunnerPostQueryRepositoryTest extends RepositoryTestConfig { + + @Autowired + private ScheduleRunnerPostRepository scheduleRunnerPostRepository; + + @Autowired + private EntityManager em; + + private Runner runner; + + @BeforeEach + void setUp() { + final Member member = MemberFixture.createDitoo(); + em.persist(member); + runner = RunnerFixture.createRunner(member); + em.persist(runner); + } + + @DisplayName("deadline 이 지난 NOT_STARTED 상태의 runnerPost 는 OVERDUE 상태로 된다.") + @Test + void updateAllPassedDeadline_success() { + // given + final Deadline passedDeadlineOne = deadline(LocalDateTime.now().minusMinutes(1)); + final Deadline passedDeadlineTwo = deadline(LocalDateTime.now().minusMinutes(1)); + final RunnerPost runnerPostOne = RunnerPostFixture.create(runner, passedDeadlineOne); + final RunnerPost runnerPostTwo = RunnerPostFixture.create(runner, passedDeadlineTwo); + em.persist(runnerPostOne); + em.persist(runnerPostTwo); + + // when + scheduleRunnerPostRepository.updateAllPassedDeadline(); + final List actual = scheduleRunnerPostRepository.findAll().stream() + .map(RunnerPost::getReviewStatus) + .toList(); + + // then + assertThat(actual).containsExactly(OVERDUE, OVERDUE); + } + + @DisplayName("deadline 이 지난 DONE 상태의 runnerPost 는 리뷰 상태가 업데이트 되지 않는다.") + @Test + void updateAllPassedDeadline_fail_when_reviewStatus_is_DONE() { + // given + 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, IsReviewed.notReviewed()); + final RunnerPost runnerPostTwo = RunnerPostFixture.create(runner, passedDeadlineTwo, expectedReviewStatus, IsReviewed.notReviewed()); + em.persist(runnerPostOne); + em.persist(runnerPostTwo); + + // when + scheduleRunnerPostRepository.updateAllPassedDeadline(); + final List actual = scheduleRunnerPostRepository.findAll().stream() + .map(RunnerPost::getReviewStatus) + .toList(); + + // then + assertThat(actual).containsExactly(expectedReviewStatus, expectedReviewStatus); + } + + @DisplayName("deadline 이 지나지 않은 NOT_STARTED 상태의 runnerPost 는 리뷰 상태가 업데이트 되지 않는다.") + @Test + void updateAllPassedDeadline_fail_when_deadline_is_not_passed() { + // given + 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, IsReviewed.notReviewed()); + final RunnerPost runnerPostTwo = RunnerPostFixture.create(runner, passedDeadlineTwo, expectedReviewStatus, IsReviewed.notReviewed()); + em.persist(runnerPostOne); + em.persist(runnerPostTwo); + + // when + scheduleRunnerPostRepository.updateAllPassedDeadline(); + final List actual = scheduleRunnerPostRepository.findAll().stream() + .map(RunnerPost::getReviewStatus) + .toList(); + + // then + assertThat(actual).containsExactly(expectedReviewStatus, expectedReviewStatus); + } +} 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 5f13b743b..417005ffa 100644 --- a/backend/baton/src/test/java/touch/baton/config/AssuredTestConfig.java +++ b/backend/baton/src/test/java/touch/baton/config/AssuredTestConfig.java @@ -12,46 +12,45 @@ 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.TestMemberQueryRepository; +import touch.baton.assure.repository.TestNotificationCommandRepository; 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.assure.repository.TestRunnerPostQueryRepository; +import touch.baton.assure.repository.TestRunnerQueryRepository; +import touch.baton.assure.repository.TestSupporterQueryRepository; +import touch.baton.assure.repository.TestSupporterRunnerPostQueryRepository; +import touch.baton.assure.repository.TestTagQuerydslRepository; import touch.baton.config.converter.ConverterConfig; -import touch.baton.config.infra.auth.MockAuthTestConfig; +import touch.baton.config.infra.auth.MockBeanAuthTestConfig; import touch.baton.config.infra.github.MockGithubBranchServiceConfig; @ActiveProfiles("test") -@Import({JpaConfig.class, ConverterConfig.class, PageableTestConfig.class, MockAuthTestConfig.class, MockGithubBranchServiceConfig.class, JwtTestManager.class}) +@Import({JpaConfig.class, QuerydslConfig.class, ConverterConfig.class, PageableTestConfig.class, MockBeanAuthTestConfig.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 TestMemberRepository memberRepository; + protected TestMemberQueryRepository memberRepository; @Autowired - protected TestRunnerRepository runnerRepository; + protected TestRunnerQueryRepository runnerRepository; @Autowired - protected TestSupporterRepository supporterRepository; + protected TestSupporterQueryRepository supporterRepository; @Autowired - protected TestRunnerPostRepository runnerPostRepository; + protected TestRunnerPostQueryRepository runnerPostRepository; @Autowired - protected TestRunnerPostReadRepository runnerPostReadRepository; + protected TestSupporterRunnerPostQueryRepository supporterRunnerPostRepository; @Autowired - protected TestSupporterRunnerPostRepository supporterRunnerPostRepository; + protected TestTagQuerydslRepository tagQueryRepository; @Autowired - protected TestTagRepository tagRepository; + protected TestNotificationCommandRepository notificationCommandRepository; @Autowired protected TestRefreshTokenRepository refreshTokenRepository; diff --git a/backend/baton/src/test/java/touch/baton/config/QueryDslRepositoryTestConfig.java b/backend/baton/src/test/java/touch/baton/config/QueryDslRepositoryTestConfig.java new file mode 100644 index 000000000..a06536c83 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/config/QueryDslRepositoryTestConfig.java @@ -0,0 +1,37 @@ +package touch.baton.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import touch.baton.domain.notification.query.repository.NotificationQuerydslRepository; +import touch.baton.domain.runnerpost.query.repository.RunnerPostPageRepository; +import touch.baton.domain.tag.query.repository.TagQuerydslRepository; + +@TestConfiguration +public class QueryDslRepositoryTestConfig { + + @PersistenceContext + private EntityManager em; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(em); + } + + @Bean + public RunnerPostPageRepository runnerPostPageRepository() { + return new RunnerPostPageRepository(jpaQueryFactory()); + } + + @Bean + public NotificationQuerydslRepository notificationQuerydslRepository() { + return new NotificationQuerydslRepository(jpaQueryFactory()); + } + + @Bean + public TagQuerydslRepository tagQuerydslRepository() { + return new TagQuerydslRepository(jpaQueryFactory()); + } +} diff --git a/backend/baton/src/test/java/touch/baton/config/RepositoryTestConfig.java b/backend/baton/src/test/java/touch/baton/config/RepositoryTestConfig.java index a366cfec7..67728ef28 100644 --- a/backend/baton/src/test/java/touch/baton/config/RepositoryTestConfig.java +++ b/backend/baton/src/test/java/touch/baton/config/RepositoryTestConfig.java @@ -1,9 +1,89 @@ package touch.baton.config; +import jakarta.persistence.EntityManager; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; +import touch.baton.domain.notification.command.Notification; +import touch.baton.domain.notification.command.vo.NotificationReferencedId; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.SupporterRunnerPost; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.tag.command.RunnerPostTag; +import touch.baton.domain.tag.command.Tag; +import touch.baton.fixture.domain.NotificationFixture; +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(JpaConfig.class) +import java.time.LocalDateTime; + +import static touch.baton.fixture.vo.DeadlineFixture.deadline; +import static touch.baton.fixture.vo.TagNameFixture.tagName; + +@Import({JpaConfig.class, QueryDslRepositoryTestConfig.class}) @DataJpaTest public abstract class RepositoryTestConfig { + + @Autowired + protected EntityManager em; + + protected Member persistMember(final Member member) { + em.persist(member); + return member; + } + + protected Runner persistRunner(final Member member) { + em.persist(member); + final Runner runner = RunnerFixture.createRunner(member); + em.persist(runner); + return runner; + } + + protected Supporter persistSupporter(final Member member) { + em.persist(member); + final Supporter supporter = SupporterFixture.create(member); + em.persist(supporter); + return supporter; + } + + protected RunnerPost persistRunnerPost(final Runner runner) { + final RunnerPost runnerPost = RunnerPostFixture.create(runner, deadline(LocalDateTime.now().plusHours(100))); + em.persist(runnerPost); + return runnerPost; + } + + protected SupporterRunnerPost persistApplicant(final Supporter supporter, final RunnerPost runnerPost) { + final SupporterRunnerPost applicant = SupporterRunnerPostFixture.create(runnerPost, supporter); + em.persist(applicant); + return applicant; + } + + protected void persistAssignSupporter(final Supporter supporter, final RunnerPost runnerPost) { + runnerPost.assignSupporter(supporter); + em.persist(runnerPost); + } + + protected Tag persistTag(final String tagName) { + final Tag tag = TagFixture.create(tagName(tagName)); + em.persist(tag); + return tag; + } + + protected RunnerPostTag persistRunnerPostTag(final RunnerPost runnerPost, final Tag tag) { + final RunnerPostTag runnerPostTag = RunnerPostTagFixture.create(runnerPost, tag); + em.persist(runnerPostTag); + return runnerPostTag; + } + + protected Notification persistNotification(final Member targetMember, final NotificationReferencedId notificationReferencedId) { + final Notification notification = NotificationFixture.create(targetMember, notificationReferencedId); + em.persist(notification); + return notification; + } } 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 0b2faed47..42ccfa96f 100644 --- a/backend/baton/src/test/java/touch/baton/config/RestdocsConfig.java +++ b/backend/baton/src/test/java/touch/baton/config/RestdocsConfig.java @@ -3,32 +3,46 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; -import org.springframework.data.web.PageableHandlerMethodArgumentResolver; -import org.springframework.format.support.FormattingConversionService; -import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.RestDocumentationContextProvider; -import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import touch.baton.config.converter.ConverterConfig; -import touch.baton.config.converter.OauthTypeConverter; -import touch.baton.config.converter.ReviewStatusConverter; -import touch.baton.domain.oauth.controller.resolver.AuthMemberPrincipalArgumentResolver; -import touch.baton.domain.oauth.controller.resolver.AuthRunnerPrincipalArgumentResolver; -import touch.baton.domain.oauth.controller.resolver.AuthSupporterPrincipalArgumentResolver; -import touch.baton.domain.oauth.repository.OauthMemberRepository; -import touch.baton.domain.oauth.repository.OauthRunnerRepository; -import touch.baton.domain.oauth.repository.OauthSupporterRepository; +import org.springframework.web.context.WebApplicationContext; +import touch.baton.domain.member.command.controller.MemberBranchController; +import touch.baton.domain.member.command.controller.RunnerCommandController; +import touch.baton.domain.member.command.controller.SupporterCommandController; +import touch.baton.domain.member.command.service.GithubBranchManageable; +import touch.baton.domain.member.command.service.RunnerCommandService; +import touch.baton.domain.member.command.service.SupporterCommandService; +import touch.baton.domain.member.query.controller.MemberQueryController; +import touch.baton.domain.member.query.controller.RunnerQueryController; +import touch.baton.domain.member.query.controller.SupporterQueryController; +import touch.baton.domain.member.query.service.RunnerQueryService; +import touch.baton.domain.member.query.service.SupporterQueryService; +import touch.baton.domain.notification.command.controller.NotificationCommandController; +import touch.baton.domain.notification.command.service.NotificationCommandService; +import touch.baton.domain.notification.query.controller.NotificationQueryController; +import touch.baton.domain.notification.query.service.NotificationQueryService; +import touch.baton.domain.oauth.command.controller.OauthCommandController; +import touch.baton.domain.oauth.command.repository.OauthMemberCommandRepository; +import touch.baton.domain.oauth.command.repository.OauthRunnerCommandRepository; +import touch.baton.domain.oauth.command.repository.OauthSupporterCommandRepository; +import touch.baton.domain.oauth.command.service.OauthCommandService; +import touch.baton.domain.runnerpost.command.controller.RunnerPostCommandController; +import touch.baton.domain.runnerpost.command.service.RunnerPostCommandService; +import touch.baton.domain.runnerpost.query.controller.RunnerPostQueryController; +import touch.baton.domain.runnerpost.query.service.RunnerPostQueryService; +import touch.baton.domain.tag.query.controller.TagQueryController; +import touch.baton.domain.tag.query.service.TagQueryService; import touch.baton.infra.auth.jwt.JwtDecoder; import java.util.UUID; @@ -40,14 +54,24 @@ import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -@ExtendWith(RestDocumentationExtension.class) -@Import({RestDocsResultConfig.class, ConverterConfig.class}) +@WebMvcTest({ + RunnerQueryController.class, + RunnerCommandController.class, + SupporterQueryController.class, + SupporterCommandController.class, + MemberQueryController.class, + RunnerPostCommandController.class, + RunnerPostQueryController.class, + TagQueryController.class, + MemberBranchController.class, + OauthCommandController.class, + NotificationCommandController.class, + NotificationQueryController.class +}) +@Import({RestDocsResultConfig.class}) public abstract class RestdocsConfig { protected MockMvc mockMvc; - protected AuthMemberPrincipalArgumentResolver authMemberPrincipalArgumentResolver; - protected AuthRunnerPrincipalArgumentResolver authRunnerPrincipalArgumentResolver; - protected AuthSupporterPrincipalArgumentResolver authSupporterPrincipalArgumentResolver; @Autowired protected RestDocumentationResultHandler restDocs; @@ -62,38 +86,51 @@ public abstract class RestdocsConfig { protected JwtDecoder jwtDecoder; @MockBean - protected OauthMemberRepository oauthMemberRepository; + protected OauthMemberCommandRepository oauthMemberCommandRepository; @MockBean - protected OauthRunnerRepository oauthRunnerRepository; + protected OauthRunnerCommandRepository oauthRunnerCommandRepository; @MockBean - protected OauthSupporterRepository oauthSupporterRepository; + protected OauthSupporterCommandRepository oauthSupporterCommandRepository; - @Autowired - protected MappingJackson2HttpMessageConverter jackson2HttpMessageConverter; + @MockBean + protected GithubBranchManageable githubBranchManageable; - @Autowired - protected Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder; - - protected void restdocsSetUp(final Object controller) { - authMemberPrincipalArgumentResolver = new AuthMemberPrincipalArgumentResolver(jwtDecoder, oauthMemberRepository); - authRunnerPrincipalArgumentResolver = new AuthRunnerPrincipalArgumentResolver(jwtDecoder, oauthRunnerRepository); - authSupporterPrincipalArgumentResolver = new AuthSupporterPrincipalArgumentResolver(jwtDecoder, oauthSupporterRepository); - - final FormattingConversionService formattingConversionService = new FormattingConversionService(); - formattingConversionService.addConverter(new OauthTypeConverter()); - formattingConversionService.addConverter(new ReviewStatusConverter()); - - this.mockMvc = MockMvcBuilders.standaloneSetup(controller) - .setCustomArgumentResolvers( - authMemberPrincipalArgumentResolver, - authRunnerPrincipalArgumentResolver, - authSupporterPrincipalArgumentResolver, - new PageableHandlerMethodArgumentResolver()) + @MockBean + protected OauthCommandService oauthCommandService; + + @MockBean + protected RunnerPostQueryService runnerPostQueryService; + + @MockBean + protected RunnerPostCommandService runnerPostCommandService; + + @MockBean + protected RunnerQueryService runnerQueryService; + + @MockBean + protected RunnerCommandService runnerCommandService; + + @MockBean + protected SupporterQueryService supporterQueryService; + + @MockBean + protected SupporterCommandService supporterCommandService; + + @MockBean + protected TagQueryService tagQueryService; + + @MockBean + protected NotificationQueryService notificationQueryService; + + @MockBean + protected NotificationCommandService notificationCommandService; + + @BeforeEach + void restdocsSetUp(final WebApplicationContext webApplicationContext) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) .apply(documentationConfiguration(restDocumentation)) - .setConversionService(formattingConversionService) - .setMessageConverters(jackson2HttpMessageConverter) .alwaysDo(restDocs) .build(); } 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 e9aab1534..647434219 100644 --- a/backend/baton/src/test/java/touch/baton/config/ServiceTestConfig.java +++ b/backend/baton/src/test/java/touch/baton/config/ServiceTestConfig.java @@ -1,54 +1,82 @@ package touch.baton.config; import org.springframework.beans.factory.annotation.Autowired; -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; -import touch.baton.domain.tag.repository.RunnerPostTagRepository; -import touch.baton.domain.tag.repository.TagRepository; -import touch.baton.domain.technicaltag.repository.RunnerTechnicalTagRepository; -import touch.baton.domain.technicaltag.repository.SupporterTechnicalTagRepository; -import touch.baton.domain.technicaltag.repository.TechnicalTagRepository; +import org.springframework.context.ApplicationEventPublisher; +import touch.baton.domain.feedback.command.repository.SupporterFeedbackCommandRepository; +import touch.baton.domain.member.command.repository.MemberCommandRepository; +import touch.baton.domain.member.command.repository.SupporterCommandRepository; +import touch.baton.domain.member.command.repository.SupporterRunnerPostCommandRepository; +import touch.baton.domain.member.query.repository.RunnerQueryRepository; +import touch.baton.domain.member.query.repository.SupporterQueryRepository; +import touch.baton.domain.member.query.repository.SupporterRunnerPostQueryRepository; +import touch.baton.domain.notification.command.repository.NotificationCommandRepository; +import touch.baton.domain.notification.query.repository.NotificationQuerydslRepository; +import touch.baton.domain.runnerpost.command.repository.RunnerPostCommandRepository; +import touch.baton.domain.runnerpost.query.repository.RunnerPostPageRepository; +import touch.baton.domain.runnerpost.query.repository.RunnerPostQueryRepository; +import touch.baton.domain.tag.command.repository.TagCommandRepository; +import touch.baton.domain.tag.query.repository.RunnerPostTagQueryRepository; +import touch.baton.domain.tag.query.repository.TagQuerydslRepository; +import touch.baton.domain.technicaltag.command.repository.RunnerTechnicalTagCommandRepository; +import touch.baton.domain.technicaltag.command.repository.SupporterTechnicalTagCommandRepository; +import touch.baton.domain.technicaltag.query.repository.TechnicalTagQueryRepository; public abstract class ServiceTestConfig extends RepositoryTestConfig { @Autowired - protected MemberRepository memberRepository; + protected MemberCommandRepository memberCommandRepository; @Autowired - protected RunnerRepository runnerRepository; + protected RunnerQueryRepository runnerQueryRepository; @Autowired - protected SupporterRepository supporterRepository; + protected SupporterQueryRepository supporterQueryRepository; @Autowired - protected RunnerPostRepository runnerPostRepository; + protected RunnerPostQueryRepository runnerPostQueryRepository; @Autowired - protected RunnerPostReadRepository runnerPostReadRepository; + protected RunnerPostPageRepository runnerPostPageRepository; @Autowired - protected SupporterRunnerPostRepository supporterRunnerPostRepository; + protected SupporterRunnerPostQueryRepository supporterRunnerPostQueryRepository; @Autowired - protected RunnerPostTagRepository runnerPostTagRepository; + protected RunnerPostTagQueryRepository runnerPostTagQueryRepository; @Autowired - protected TagRepository tagRepository; + protected TagQuerydslRepository tagQuerydslRepository; @Autowired - protected SupporterFeedbackRepository supporterFeedbackRepository; + protected SupporterFeedbackCommandRepository supporterFeedbackCommandRepository; @Autowired - protected TechnicalTagRepository technicalTagRepository; + protected TechnicalTagQueryRepository technicalTagQueryRepository; @Autowired - protected RunnerTechnicalTagRepository runnerTechnicalTagRepository; + protected RunnerTechnicalTagCommandRepository runnerTechnicalTagCommandRepository; @Autowired - protected SupporterTechnicalTagRepository supporterTechnicalTagRepository; + protected SupporterTechnicalTagCommandRepository supporterTechnicalTagCommandRepository; + + @Autowired + protected RunnerPostCommandRepository runnerPostCommandRepository; + + @Autowired + protected TagCommandRepository tagCommandRepository; + + @Autowired + protected SupporterCommandRepository supporterCommandRepository; + + @Autowired + protected SupporterRunnerPostCommandRepository supporterRunnerPostCommandRepository; + + @Autowired + protected NotificationCommandRepository notificationCommandRepository; + + @Autowired + protected NotificationQuerydslRepository notificationQuerydslRepository; + + @Autowired + protected ApplicationEventPublisher publisher; } diff --git a/backend/baton/src/test/java/touch/baton/config/converter/ConverterConfigTest.java b/backend/baton/src/test/java/touch/baton/config/converter/ConverterConfigTest.java index 5ec46c862..7af3d5bdf 100644 --- a/backend/baton/src/test/java/touch/baton/config/converter/ConverterConfigTest.java +++ b/backend/baton/src/test/java/touch/baton/config/converter/ConverterConfigTest.java @@ -12,9 +12,9 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.FilterType; import touch.baton.config.ArgumentResolverConfig; -import touch.baton.domain.oauth.controller.resolver.AuthMemberPrincipalArgumentResolver; -import touch.baton.domain.oauth.controller.resolver.AuthRunnerPrincipalArgumentResolver; -import touch.baton.domain.oauth.controller.resolver.AuthSupporterPrincipalArgumentResolver; +import touch.baton.domain.oauth.query.controller.resolver.AuthMemberPrincipalArgumentResolver; +import touch.baton.domain.oauth.query.controller.resolver.AuthRunnerPrincipalArgumentResolver; +import touch.baton.domain.oauth.query.controller.resolver.AuthSupporterPrincipalArgumentResolver; import java.time.LocalDateTime; diff --git a/backend/baton/src/test/java/touch/baton/config/infra/auth/MockBeanAuthTestConfig.java b/backend/baton/src/test/java/touch/baton/config/infra/auth/MockBeanAuthTestConfig.java new file mode 100644 index 000000000..5ad553401 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/config/infra/auth/MockBeanAuthTestConfig.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.FakeJwtConfig; +import touch.baton.config.infra.auth.oauth.authcode.MockAuthCodeRequestUrlProviderCompositeConfig; +import touch.baton.config.infra.auth.oauth.client.MockAuthInformationClientCompositeConfig; + +@Profile("test") +@Import({FakeJwtConfig.class, + MockAuthCodeRequestUrlProviderCompositeConfig.class, + MockAuthInformationClientCompositeConfig.class}) +@TestConfiguration +public abstract class MockBeanAuthTestConfig { +} diff --git a/backend/baton/src/test/java/touch/baton/config/infra/auth/jwt/FakeJwtConfig.java b/backend/baton/src/test/java/touch/baton/config/infra/auth/jwt/FakeJwtConfig.java new file mode 100644 index 000000000..dfa179693 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/config/infra/auth/jwt/FakeJwtConfig.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 FakeJwtConfig { + + @Bean + JwtEncoder jwtEncoder() { + return new JwtEncoder(fakeJwtConfig()); + } + + @Bean + JwtDecoder jwtDecoder() { + return new JwtDecoder(fakeJwtConfig()); + } + + private JwtConfig fakeJwtConfig() { + 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 index 86453ceac..b1ce87bc0 100644 --- 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 @@ -2,7 +2,7 @@ import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; -import touch.baton.domain.oauth.token.RefreshToken; +import touch.baton.domain.oauth.command.token.RefreshToken; import touch.baton.infra.auth.jwt.JwtConfig; import touch.baton.infra.auth.jwt.JwtEncoder; diff --git a/backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/authcode/FakeAuthCodes.java b/backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/authcode/FakeAuthCodes.java new file mode 100644 index 000000000..83e1e9dd3 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/authcode/FakeAuthCodes.java @@ -0,0 +1,20 @@ +package touch.baton.config.infra.auth.oauth.authcode; + +public abstract class FakeAuthCodes { + + 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/authcode/MockAuthCodeRequestUrlProviderCompositeConfig.java b/backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/authcode/MockAuthCodeRequestUrlProviderCompositeConfig.java index 25a5c7bd5..a64bf0e64 100644 --- 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 @@ -3,8 +3,8 @@ 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 touch.baton.domain.oauth.command.OauthType; +import touch.baton.domain.oauth.command.authcode.AuthCodeRequestUrlProviderComposite; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.when; diff --git a/backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/client/MockAuthInformationClientCompositeConfig.java b/backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/client/MockAuthInformationClientCompositeConfig.java new file mode 100644 index 000000000..2c0b655fc --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/config/infra/auth/oauth/client/MockAuthInformationClientCompositeConfig.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.FakeAuthCodes; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.oauth.command.OauthInformation; +import touch.baton.domain.oauth.command.OauthType; +import touch.baton.domain.oauth.command.client.OauthInformationClientComposite; +import touch.baton.domain.oauth.command.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 MockAuthInformationClientCompositeConfig { + + @Bean + OauthInformationClientComposite oauthInformationClientComposite() { + final OauthInformationClientComposite mock = Mockito.mock(OauthInformationClientComposite.class); + + when(mock.fetchInformation(any(OauthType.class), eq(FakeAuthCodes.ditooAuthCode()))) + .thenReturn(oauthInformation(MemberFixture.createDitoo(), "ditoo_access_token")); + + when(mock.fetchInformation(any(OauthType.class), eq(FakeAuthCodes.ethanAuthCode()))) + .thenReturn(oauthInformation(MemberFixture.createEthan(), "ethan_access_token")); + + when(mock.fetchInformation(any(OauthType.class), eq(FakeAuthCodes.hyenaAuthCode()))) + .thenReturn(oauthInformation(MemberFixture.createHyena(), "hyena_access_token")); + + when(mock.fetchInformation(any(OauthType.class), eq(FakeAuthCodes.judyAuthCode()))) + .thenReturn(oauthInformation(MemberFixture.createJudy(), "judy_access_token")); + + return mock; + } + + private 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/document/github/GithubBranchApiTest.java b/backend/baton/src/test/java/touch/baton/document/github/GithubBranchApiTest.java index 339165dfd..01052060e 100644 --- a/backend/baton/src/test/java/touch/baton/document/github/GithubBranchApiTest.java +++ b/backend/baton/src/test/java/touch/baton/document/github/GithubBranchApiTest.java @@ -1,41 +1,33 @@ 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.domain.member.command.Member; +import touch.baton.domain.member.command.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.apache.http.HttpHeaders.AUTHORIZATION; +import static org.apache.http.HttpHeaders.CONTENT_TYPE; +import static org.apache.http.HttpHeaders.LOCATION; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; 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.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.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 { @@ -46,7 +38,7 @@ void createMemberBranch() throws Exception { final GithubRepoNameRequest request = new GithubRepoNameRequest("drunken-ditoo"); // when - when(oauthMemberRepository.findBySocialId(any())).thenReturn(Optional.ofNullable(member)); + when(oauthMemberCommandRepository.findBySocialId(any())).thenReturn(Optional.ofNullable(member)); doNothing().when(githubBranchManageable).createBranch(eq(socialId), anyString()); // then diff --git a/backend/baton/src/test/java/touch/baton/document/notification/delete/NotificationDeleteApiTest.java b/backend/baton/src/test/java/touch/baton/document/notification/delete/NotificationDeleteApiTest.java new file mode 100644 index 000000000..424ba978d --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/document/notification/delete/NotificationDeleteApiTest.java @@ -0,0 +1,57 @@ +package touch.baton.document.notification.delete; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import touch.baton.config.RestdocsConfig; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.notification.command.Notification; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.NotificationFixture; + +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static touch.baton.fixture.vo.NotificationReferencedIdFixture.notificationReferencedId; + +class NotificationDeleteApiTest extends RestdocsConfig { + + @DisplayName("알림 삭제 API") + @Test + void deleteNotificationByNotificationId() throws Exception { + // given + final Member memberHyena = MemberFixture.createHyena(); + final String token = getAccessTokenBySocialId(memberHyena.getSocialId().getValue()); + final Notification notification = NotificationFixture.create(memberHyena, notificationReferencedId(1L)); + + // when + doNothing().when(notificationCommandService).deleteNotificationByMember(any(Member.class), anyLong()); + when(oauthMemberCommandRepository.findBySocialId(any())).thenReturn(Optional.ofNullable(memberHyena)); + + final Notification spyNotification = spy(notification); + when(spyNotification.getId()).thenReturn(1L); + + // then + mockMvc.perform(delete("/api/v1/notifications/{notificationId}", spyNotification.getId()) + .header(AUTHORIZATION, "Bearer " + token)) + .andExpect(status().isNoContent()) + .andDo(restDocs.document( + pathParameters( + parameterWithName("notificationId").description("알림 식별자값") + ), + requestHeaders( + headerWithName(AUTHORIZATION).description("Bearer JWT") + ) + )); + } +} diff --git a/backend/baton/src/test/java/touch/baton/document/notification/read/NotificationReadWithLoginedMemberApiTest.java b/backend/baton/src/test/java/touch/baton/document/notification/read/NotificationReadWithLoginedMemberApiTest.java new file mode 100644 index 000000000..5eaa9d2c0 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/document/notification/read/NotificationReadWithLoginedMemberApiTest.java @@ -0,0 +1,83 @@ +package touch.baton.document.notification.read; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import touch.baton.config.RestdocsConfig; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.notification.command.Notification; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.NotificationFixture; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Optional; + +import static org.apache.http.HttpHeaders.CONTENT_TYPE; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +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.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static touch.baton.fixture.vo.NotificationReferencedIdFixture.notificationReferencedId; + +class NotificationReadWithLoginedMemberApiTest extends RestdocsConfig { + + @DisplayName("로그인한 사용자 알림 목록 조회 API") + @Test + void readNotificationsByMemberId() throws Exception { + // given + final Member memberHyena = MemberFixture.createHyena(); + final String token = getAccessTokenBySocialId(memberHyena.getSocialId().getValue()); + final Notification notification = NotificationFixture.create(memberHyena, notificationReferencedId(1L)); + + final Member spyMember = spy(memberHyena); + when(spyMember.getId()).thenReturn(1L); + + final Notification spyNotification = spy(notification); + when(spyNotification.getId()).thenReturn(1L); + doReturn(LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES)) + .when(spyNotification) + .getCreatedAt(); + + // when + when(notificationQueryService.readNotificationsByMemberId(anyLong(), anyInt())).thenReturn(List.of(spyNotification)); + when(oauthMemberCommandRepository.findBySocialId(any())).thenReturn(Optional.of(spyMember)); + + // then + mockMvc.perform(get("/api/v1/notifications") + .header(AUTHORIZATION, "Bearer " + token) + .contentType(APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andDo(restDocs.document( + requestHeaders( + headerWithName(AUTHORIZATION).description("Bearer JWT"), + headerWithName(CONTENT_TYPE).description(APPLICATION_JSON_VALUE) + ), + responseFields( + fieldWithPath("data.[].notificationId").type(NUMBER).description("알림 식별자값(id)"), + fieldWithPath("data.[].title").type(STRING).description("알림 제목"), + fieldWithPath("data.[].message").type(STRING).description("알림 내용"), + fieldWithPath("data.[].notificationType").type(STRING).description("알림 타입 (with referencedId)"), + fieldWithPath("data.[].referencedId").type(NUMBER).description("알림 연관된 식별자값"), + fieldWithPath("data.[].isRead").type(BOOLEAN).description("알림 읽음 여부"), + fieldWithPath("data.[].createdAt").type(STRING).description("알림 생성 시간") + )) + ); + } +} diff --git a/backend/baton/src/test/java/touch/baton/document/notification/update/NotificationUpdateApiTest.java b/backend/baton/src/test/java/touch/baton/document/notification/update/NotificationUpdateApiTest.java new file mode 100644 index 000000000..811b7f469 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/document/notification/update/NotificationUpdateApiTest.java @@ -0,0 +1,57 @@ +package touch.baton.document.notification.update; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import touch.baton.config.RestdocsConfig; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.notification.command.Notification; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.NotificationFixture; + +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static touch.baton.fixture.vo.NotificationReferencedIdFixture.notificationReferencedId; + +class NotificationUpdateApiTest extends RestdocsConfig { + + @DisplayName("알림 읽기 여부 기록 API") + @Test + void updateNotificationIsReadTrueByMember() throws Exception { + // given + final Member memberHyena = MemberFixture.createHyena(); + final String token = getAccessTokenBySocialId(memberHyena.getSocialId().getValue()); + final Notification notification = NotificationFixture.create(memberHyena, notificationReferencedId(1L)); + + // when + doNothing().when(notificationCommandService).updateNotificationIsReadTrueByMember(any(Member.class), anyLong()); + when(oauthMemberCommandRepository.findBySocialId(any())).thenReturn(Optional.ofNullable(memberHyena)); + + final Notification spyNotification = spy(notification); + when(spyNotification.getId()).thenReturn(1L); + + // then + mockMvc.perform(patch("/api/v1/notifications/{notificationId}", spyNotification.getId()) + .header(AUTHORIZATION, "Bearer " + token)) + .andExpect(status().isNoContent()) + .andDo(restDocs.document( + pathParameters( + parameterWithName("notificationId").description("알림 식별자값") + ), + requestHeaders( + headerWithName(AUTHORIZATION).description("Bearer JWT") + ) + )); + } +} diff --git a/backend/baton/src/test/java/touch/baton/document/oauth/OauthLogoutApiTest.java b/backend/baton/src/test/java/touch/baton/document/oauth/OauthLogoutApiTest.java new file mode 100644 index 000000000..a7bbb76c6 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/document/oauth/OauthLogoutApiTest.java @@ -0,0 +1,39 @@ +package touch.baton.document.oauth; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import touch.baton.config.RestdocsConfig; +import touch.baton.domain.member.command.Member; +import touch.baton.fixture.domain.MemberFixture; + +import java.util.Optional; + +import static org.apache.http.HttpHeaders.AUTHORIZATION; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class OauthLogoutApiTest extends RestdocsConfig { + + @DisplayName("로그아웃을 하면 리프레시 토큰이 삭제된다.") + @Test + void logout() throws Exception { + // given, when + final Member ethan = MemberFixture.createEthan(); + final String accessToken = getAccessTokenBySocialId(ethan.getSocialId().getValue()); + given(oauthMemberCommandRepository.findBySocialId(any())).willReturn(Optional.ofNullable(ethan)); + + // then + mockMvc.perform(patch("/api/v1/oauth/logout") + .header(AUTHORIZATION, "Bearer " + accessToken)) + .andExpect(status().isNoContent()) + .andDo(restDocs.document( + requestHeaders( + headerWithName("Authorization").description("Access Token") + ) + )); + } +} 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 eddd94c76..9c733933f 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,24 +1,15 @@ 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; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -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 touch.baton.domain.member.command.Member; +import touch.baton.domain.oauth.command.token.AccessToken; +import touch.baton.domain.oauth.command.token.ExpireDate; +import touch.baton.domain.oauth.command.token.RefreshToken; +import touch.baton.domain.oauth.command.token.Token; +import touch.baton.domain.oauth.command.token.Tokens; import java.time.LocalDateTime; @@ -35,39 +26,23 @@ import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static touch.baton.domain.oauth.OauthType.GITHUB; +import static touch.baton.domain.oauth.command.OauthType.GITHUB; -@EnableConfigurationProperties(GithubOauthConfig.class) -@TestPropertySource("classpath:application.yml") -@WebMvcTest(OauthController.class) class GithubOauthApiTest extends RestdocsConfig { - @MockBean - private OauthService oauthService; - - @Autowired - private GithubOauthConfig githubOauthConfig; - - @BeforeEach - void setUp() { - final OauthController oauthController = new OauthController(oauthService); - restdocsSetUp(oauthController); - } - @DisplayName("Github 소셜 로그인을 위한 AuthCode 를 받을 수 있도록 사용자를 redirect 한다.") @Test void github_redirect_auth_code() throws Exception { - // given & when - when(oauthService.readAuthCodeRedirect(GITHUB)) - .thenReturn(githubOauthConfig.redirectUri()); + // given, when + when(oauthCommandService.readAuthCodeRedirect(GITHUB)) + .thenReturn("https://test-redirect-url.com"); // then mockMvc.perform(get("/api/v1/oauth/{oauthType}", "github")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl(githubOauthConfig.redirectUri())) + .andExpect(redirectedUrl("https://test-redirect-url.com")) .andDo(restDocs.document( pathParameters( parameterWithName("oauthType").description("소셜 로그인 타입") @@ -75,8 +50,7 @@ void github_redirect_auth_code() throws Exception { responseHeaders( headerWithName(LOCATION).description("Oauth 서버 리다이렉트 URL") ) - )) - .andDo(print()); + )); } // FIXME: 2023/09/15 RFC2616 버전오류 해결해주세요. @@ -84,7 +58,7 @@ void github_redirect_auth_code() throws Exception { @DisplayName("Github 소셜 로그인을 위해 AuthCode 를 받아 SocialToken 으로 교환하여 Github 프로필 정보를 찾아오고 미가입 사용자일 경우 자동으로 회원가입을 진행하고 JWT 로 변환하여 클라이언트에게 넘겨준다.") @Test void github_login() throws Exception { - // given & when + // given, when final RefreshToken refreshToken = RefreshToken.builder() .member(mock(Member.class)) .token(new Token("mock refresh token")) @@ -92,7 +66,7 @@ void github_login() throws Exception { .build(); final Tokens tokens = new Tokens(new AccessToken("Bearer Jwt"), refreshToken); - when(oauthService.login(GITHUB, "authcode")) + when(oauthCommandService.login(GITHUB, "authcode")) .thenReturn(tokens); // then @@ -116,7 +90,6 @@ void github_login() throws Exception { 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 index 4495eebc5..ce0f7a6ce 100644 --- 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 @@ -1,20 +1,15 @@ 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.domain.oauth.command.AuthorizationHeader; +import touch.baton.domain.oauth.command.token.AccessToken; +import touch.baton.domain.oauth.command.token.ExpireDate; +import touch.baton.domain.oauth.command.token.RefreshToken; +import touch.baton.domain.oauth.command.token.Token; +import touch.baton.domain.oauth.command.token.Tokens; import touch.baton.fixture.domain.MemberFixture; import java.time.Duration; @@ -30,25 +25,14 @@ 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 + // given, when final RefreshToken refreshToken = RefreshToken.builder() .token(new Token("refresh-token")) .member(MemberFixture.createEthan()) @@ -57,7 +41,7 @@ void refresh() throws Exception { 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); + given(oauthCommandService.reissueAccessToken(any(AuthorizationHeader.class), any(String.class))).willReturn(tokens); // then mockMvc.perform(post("/api/v1/oauth/refresh") @@ -77,8 +61,7 @@ void refresh() throws Exception { responseCookies( cookieWithName("refreshToken").description("새로 발급된 리프레시 토큰") ) - )) - .andDo(print()); + )); } private Cookie createCookie() { diff --git a/backend/baton/src/test/java/touch/baton/document/profile/member/read/MemberReadWithLoginedMemberApiTest.java b/backend/baton/src/test/java/touch/baton/document/profile/member/read/MemberReadWithLoginedMemberApiTest.java index 849b6b086..08bd7bc34 100644 --- a/backend/baton/src/test/java/touch/baton/document/profile/member/read/MemberReadWithLoginedMemberApiTest.java +++ b/backend/baton/src/test/java/touch/baton/document/profile/member/read/MemberReadWithLoginedMemberApiTest.java @@ -1,12 +1,9 @@ package touch.baton.document.profile.member.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 touch.baton.config.RestdocsConfig; -import touch.baton.domain.member.Member; -import touch.baton.domain.member.controller.MemberProfileController; +import touch.baton.domain.member.command.Member; import touch.baton.fixture.domain.MemberFixture; import java.util.Optional; @@ -29,13 +26,7 @@ import static touch.baton.fixture.vo.OauthIdFixture.oauthId; import static touch.baton.fixture.vo.SocialIdFixture.socialId; -@WebMvcTest(MemberProfileController.class) -public class MemberReadWithLoginedMemberApiTest extends RestdocsConfig { - - @BeforeEach - void setUp() { - restdocsSetUp(new MemberProfileController()); - } +class MemberReadWithLoginedMemberApiTest extends RestdocsConfig { @DisplayName("로그인 한 맴버 정보 조회 API") @Test @@ -53,7 +44,7 @@ void readLoginMemberByAccessToken() throws Exception { final String token = getAccessTokenBySocialId(socialId); // when - when(oauthMemberRepository.findBySocialId(any())).thenReturn(Optional.ofNullable(member)); + when(oauthMemberCommandRepository.findBySocialId(any())).thenReturn(Optional.ofNullable(member)); // then mockMvc.perform(get("/api/v1/profile/me").header(AUTHORIZATION, "Bearer " + token)) @@ -64,7 +55,6 @@ void readLoginMemberByAccessToken() throws Exception { fieldWithPath("name").type(STRING).description("사용자 이름"), fieldWithPath("imageUrl").type(STRING).description("사용자 프로필 이미지 url") ) - )) - .andDo(print()); + )); } } diff --git a/backend/baton/src/test/java/touch/baton/document/profile/runner/read/RunnerReadByGuestApiTest.java b/backend/baton/src/test/java/touch/baton/document/profile/runner/read/RunnerReadByGuestApiTest.java index 7ed538a3a..92ef066ce 100644 --- a/backend/baton/src/test/java/touch/baton/document/profile/runner/read/RunnerReadByGuestApiTest.java +++ b/backend/baton/src/test/java/touch/baton/document/profile/runner/read/RunnerReadByGuestApiTest.java @@ -1,17 +1,11 @@ package touch.baton.document.profile.runner.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.member.Member; -import touch.baton.domain.runner.Runner; -import touch.baton.domain.runner.controller.RunnerProfileController; -import touch.baton.domain.runner.service.RunnerService; -import touch.baton.domain.runnerpost.service.RunnerPostService; -import touch.baton.domain.technicaltag.TechnicalTag; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.technicaltag.command.TechnicalTag; import touch.baton.fixture.domain.MemberFixture; import touch.baton.fixture.domain.RunnerFixture; import touch.baton.fixture.domain.TechnicalTagFixture; @@ -33,25 +27,12 @@ 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.test.web.servlet.result.MockMvcResultHandlers.print; 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(RunnerProfileController.class) class RunnerReadByGuestApiTest extends RestdocsConfig { - @MockBean - private RunnerPostService runnerPostService; - - @MockBean - private RunnerService runnerService; - - @BeforeEach - void setUp() { - restdocsSetUp(new RunnerProfileController(runnerPostService, runnerService)); - } - @DisplayName("러너 본인 프로필 조회 API") @Test void readMyProfileByToken() throws Exception { @@ -61,12 +42,11 @@ void readMyProfileByToken() throws Exception { final Runner runner = RunnerFixture.createRunner(MemberFixture.createHyena(), List.of(java, spring)); final String token = getAccessTokenBySocialId(runner.getMember().getSocialId().getValue()); - when(oauthRunnerRepository.joinByMemberSocialId(notNull())).thenReturn(Optional.ofNullable(runner)); + when(oauthRunnerCommandRepository.joinByMemberSocialId(notNull())).thenReturn(Optional.ofNullable(runner)); // then mockMvc.perform(get("/api/v1/profile/runner/me") .header(AUTHORIZATION, "Bearer " + token)) - .andDo(print()) .andDo(restDocs.document( requestHeaders( headerWithName(AUTHORIZATION).description("Bearer JWT") @@ -79,8 +59,7 @@ void readMyProfileByToken() throws Exception { fieldWithPath("introduction").type(STRING).description("러너 자기소개"), fieldWithPath("technicalTags").type(ARRAY).description("러너 기술 태그 목록") ) - )) - .andDo(print()); + )); } @DisplayName("러너 프로필 상세 조회 API") @@ -95,7 +74,7 @@ void readRunnerProfile() throws Exception { // when when(spyRunner.getId()).thenReturn(1L); - when(runnerService.readByRunnerId(anyLong())).thenReturn(spyRunner); + when(runnerQueryService.readByRunnerId(anyLong())).thenReturn(spyRunner); // then mockMvc.perform(get("/api/v1/profile/runner/{runnerId}", 1L)) diff --git a/backend/baton/src/test/java/touch/baton/document/profile/runner/read/RunnerReadSimpleByRunnerIdApiTest.java b/backend/baton/src/test/java/touch/baton/document/profile/runner/read/RunnerReadSimpleByRunnerIdApiTest.java new file mode 100644 index 000000000..536312c37 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/document/profile/runner/read/RunnerReadSimpleByRunnerIdApiTest.java @@ -0,0 +1,65 @@ +package touch.baton.document.profile.runner.read; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import touch.baton.config.RestdocsConfig; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.technicaltag.command.TechnicalTag; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.RunnerFixture; +import touch.baton.fixture.domain.TechnicalTagFixture; + +import java.util.List; + +import static javax.swing.text.html.parser.DTDConstants.NUMBER; +import static org.mockito.ArgumentMatchers.anyLong; +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.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.pathParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class RunnerReadSimpleByRunnerIdApiTest extends RestdocsConfig { + + @DisplayName("러너 프로필 상세 조회 API") + @Test + void readRunnerProfile() throws Exception { + // given + final Member ethan = MemberFixture.createEthan(); + final TechnicalTag javaTag = TechnicalTagFixture.createJava(); + final TechnicalTag reactTag = TechnicalTagFixture.createReact(); + final Runner runner = RunnerFixture.createRunner(ethan, List.of(javaTag, reactTag)); + final Runner spyRunner = spy(runner); + + // when + when(spyRunner.getId()).thenReturn(1L); + when(runnerQueryService.readByRunnerId(anyLong())).thenReturn(spyRunner); + + // then + mockMvc.perform(get("/api/v1/profile/runner/{runnerId}", 1L)) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andDo(restDocs.document( + pathParameters( + parameterWithName("runnerId").description("러너 식별자값") + ), + responseFields( + fieldWithPath("runnerId").type(NUMBER).description("러너 식별자값"), + fieldWithPath("name").type(STRING).description("러너 이름"), + fieldWithPath("imageUrl").type(STRING).description("사용자 이미지"), + fieldWithPath("githubUrl").type(STRING).description("깃허브 프로필 url"), + fieldWithPath("introduction").type(STRING).description("소개"), + fieldWithPath("company").type(STRING).description("소속"), + fieldWithPath("technicalTags").type(ARRAY).description("기술 스택") + ) + )); + } +} diff --git a/backend/baton/src/test/java/touch/baton/document/profile/runner/read/RunnerReadWithLoginedRunnerApiTest.java b/backend/baton/src/test/java/touch/baton/document/profile/runner/read/RunnerReadWithLoginedRunnerApiTest.java index 71e4997e7..251e0f4d2 100644 --- a/backend/baton/src/test/java/touch/baton/document/profile/runner/read/RunnerReadWithLoginedRunnerApiTest.java +++ b/backend/baton/src/test/java/touch/baton/document/profile/runner/read/RunnerReadWithLoginedRunnerApiTest.java @@ -1,16 +1,10 @@ package touch.baton.document.profile.runner.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.runner.Runner; -import touch.baton.domain.runner.controller.RunnerProfileController; -import touch.baton.domain.runner.service.RunnerService; -import touch.baton.domain.runnerpost.service.RunnerPostService; -import touch.baton.domain.technicaltag.TechnicalTag; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.technicaltag.command.TechnicalTag; import touch.baton.fixture.domain.MemberFixture; import touch.baton.fixture.domain.RunnerFixture; import touch.baton.fixture.domain.TechnicalTagFixture; @@ -28,24 +22,10 @@ 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.test.web.servlet.result.MockMvcResultHandlers.print; import static touch.baton.fixture.vo.TagNameFixture.tagName; -@WebMvcTest(RunnerProfileController.class) class RunnerReadWithLoginedRunnerApiTest extends RestdocsConfig { - @MockBean - private RunnerPostService runnerPostService; - - @MockBean - private RunnerService runnerService; - - @BeforeEach - void setUp() { - final RunnerProfileController runnerProfileController = new RunnerProfileController(runnerPostService, runnerService); - restdocsSetUp(runnerProfileController); - } - @DisplayName("러너 본인 프로필 조회 API") @Test void readMyProfileByToken() throws Exception { @@ -55,12 +35,11 @@ void readMyProfileByToken() throws Exception { final Runner runner = RunnerFixture.createRunner(MemberFixture.createHyena(), List.of(java, spring)); final String token = getAccessTokenBySocialId(runner.getMember().getSocialId().getValue()); - when(oauthRunnerRepository.joinByMemberSocialId(notNull())).thenReturn(Optional.ofNullable(runner)); + when(oauthRunnerCommandRepository.joinByMemberSocialId(notNull())).thenReturn(Optional.ofNullable(runner)); // then mockMvc.perform(get("/api/v1/profile/runner/me") .header(AUTHORIZATION, "Bearer " + token)) - .andDo(print()) .andDo(restDocs.document( requestHeaders( headerWithName(AUTHORIZATION).description("Bearer JWT") diff --git a/backend/baton/src/test/java/touch/baton/document/profile/runner/update/RunnerUpdateApiTest.java b/backend/baton/src/test/java/touch/baton/document/profile/runner/update/RunnerUpdateApiTest.java index bea871752..6d5503a59 100644 --- a/backend/baton/src/test/java/touch/baton/document/profile/runner/update/RunnerUpdateApiTest.java +++ b/backend/baton/src/test/java/touch/baton/document/profile/runner/update/RunnerUpdateApiTest.java @@ -1,18 +1,12 @@ package touch.baton.document.profile.runner.update; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import touch.baton.config.RestdocsConfig; -import touch.baton.domain.member.Member; -import touch.baton.domain.runner.Runner; -import touch.baton.domain.runner.controller.RunnerProfileController; -import touch.baton.domain.runner.service.RunnerService; -import touch.baton.domain.runner.service.dto.RunnerUpdateRequest; -import touch.baton.domain.runnerpost.service.RunnerPostService; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.service.dto.RunnerUpdateRequest; import touch.baton.fixture.domain.MemberFixture; import touch.baton.fixture.domain.RunnerFixture; @@ -32,24 +26,10 @@ import static org.springframework.restdocs.payload.JsonFieldType.STRING; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(RunnerProfileController.class) -public class RunnerUpdateApiTest extends RestdocsConfig { - - @MockBean - private RunnerService runnerService; - - @MockBean - private RunnerPostService runnerPostService; - - @BeforeEach - void setUp() { - final RunnerProfileController runnerProfileController = new RunnerProfileController(runnerPostService, runnerService); - restdocsSetUp(runnerProfileController); - } +class RunnerUpdateApiTest extends RestdocsConfig { @DisplayName("러너 프로필 수정 API") @Test @@ -63,7 +43,7 @@ void updateRunnerProfile() throws Exception { final String token = getAccessTokenBySocialId(socialId); // when - when(oauthRunnerRepository.joinByMemberSocialId(any())).thenReturn(Optional.ofNullable(judyRunner)); + when(oauthRunnerCommandRepository.joinByMemberSocialId(any())).thenReturn(Optional.ofNullable(judyRunner)); // then mockMvc.perform(patch("/api/v1/profile/runner/me") @@ -84,7 +64,6 @@ void updateRunnerProfile() throws Exception { fieldWithPath("technicalTags.[]").type(ARRAY).description("변경할 기술 태그 목록") ), responseHeaders(headerWithName(LOCATION).description("redirect uri")) - )) - .andDo(print()); + )); } } diff --git a/backend/baton/src/test/java/touch/baton/document/profile/supporter/read/SupporterReadByGuestApiTest.java b/backend/baton/src/test/java/touch/baton/document/profile/supporter/read/SupporterReadByGuestApiTest.java index 2a8b5ff88..7ba80f017 100644 --- a/backend/baton/src/test/java/touch/baton/document/profile/supporter/read/SupporterReadByGuestApiTest.java +++ b/backend/baton/src/test/java/touch/baton/document/profile/supporter/read/SupporterReadByGuestApiTest.java @@ -1,17 +1,12 @@ package touch.baton.document.profile.supporter.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.member.Member; -import touch.baton.domain.member.vo.SocialId; -import touch.baton.domain.supporter.Supporter; -import touch.baton.domain.supporter.controller.SupporterProfileController; -import touch.baton.domain.supporter.service.SupporterService; -import touch.baton.domain.technicaltag.TechnicalTag; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.technicaltag.command.TechnicalTag; import touch.baton.fixture.domain.MemberFixture; import touch.baton.fixture.domain.SupporterFixture; import touch.baton.fixture.domain.TechnicalTagFixture; @@ -38,18 +33,8 @@ import static touch.baton.fixture.vo.ReviewCountFixture.reviewCount; import static touch.baton.fixture.vo.TagNameFixture.tagName; -@WebMvcTest(SupporterProfileController.class) class SupporterReadByGuestApiTest extends RestdocsConfig { - @MockBean - private SupporterService supporterService; - - @BeforeEach - void setUp() { - final SupporterProfileController supporterProfileController = new SupporterProfileController(supporterService); - restdocsSetUp(supporterProfileController); - } - @DisplayName("서포터 프로필 조회 API") @Test void readProfileBySupporterId() throws Exception { @@ -60,7 +45,7 @@ void readProfileBySupporterId() throws Exception { final Supporter spySupporter = spy(supporter); when(spySupporter.getId()).thenReturn(1L); - when(supporterService.readBySupporterId(spySupporter.getId())).thenReturn(spySupporter); + when(supporterQueryService.readBySupporterId(spySupporter.getId())).thenReturn(spySupporter); // then mockMvc.perform(get("/api/v1/profile/supporter/{supporterId}", 1L)) @@ -78,8 +63,7 @@ void readProfileBySupporterId() throws Exception { fieldWithPath("introduction").type(STRING).description("서포터 자기소개"), fieldWithPath("technicalTags").type(ARRAY).description("서포터 기술 태그 목록") ) - )) - .andDo(print()); + )); } @DisplayName("서포터 마이페이지 프로필 조회 API") @@ -96,7 +80,7 @@ void readMyProfileByToken() throws Exception { // when when(spySupporter.getId()).thenReturn(1L); - when(oauthSupporterRepository.joinByMemberSocialId(any(SocialId.class))).thenReturn(Optional.ofNullable(spySupporter)); + when(oauthSupporterCommandRepository.joinByMemberSocialId(any(SocialId.class))).thenReturn(Optional.ofNullable(spySupporter)); // then mockMvc.perform(get("/api/v1/profile/supporter/me").header(AUTHORIZATION, "Bearer " + accessToken)) diff --git a/backend/baton/src/test/java/touch/baton/document/profile/supporter/update/SupporterUpdateApiTest.java b/backend/baton/src/test/java/touch/baton/document/profile/supporter/update/SupporterUpdateApiTest.java index 09517a8d9..17949cb09 100644 --- a/backend/baton/src/test/java/touch/baton/document/profile/supporter/update/SupporterUpdateApiTest.java +++ b/backend/baton/src/test/java/touch/baton/document/profile/supporter/update/SupporterUpdateApiTest.java @@ -1,17 +1,12 @@ package touch.baton.document.profile.supporter.update; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import touch.baton.config.RestdocsConfig; -import touch.baton.domain.member.Member; -import touch.baton.domain.supporter.Supporter; -import touch.baton.domain.supporter.controller.SupporterProfileController; -import touch.baton.domain.supporter.service.SupporterService; -import touch.baton.domain.supporter.service.dto.SupporterUpdateRequest; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.service.dto.SupporterUpdateRequest; import touch.baton.fixture.domain.MemberFixture; import touch.baton.fixture.domain.SupporterFixture; @@ -41,17 +36,7 @@ import static touch.baton.fixture.vo.OauthIdFixture.oauthId; import static touch.baton.fixture.vo.SocialIdFixture.socialId; -@WebMvcTest(SupporterProfileController.class) -public class SupporterUpdateApiTest extends RestdocsConfig { - - @MockBean - private SupporterService supporterService; - - @BeforeEach - void setUp() { - final SupporterProfileController supporterProfileController = new SupporterProfileController(supporterService); - restdocsSetUp(supporterProfileController); - } +class SupporterUpdateApiTest extends RestdocsConfig { @DisplayName("서포터 프로필 수정 API") @Test @@ -72,7 +57,7 @@ void updateSupporterProfile() throws Exception { final String token = getAccessTokenBySocialId(socialId); // when - when(oauthSupporterRepository.joinByMemberSocialId(any())).thenReturn(Optional.ofNullable(supporter)); + when(oauthSupporterCommandRepository.joinByMemberSocialId(any())).thenReturn(Optional.ofNullable(supporter)); // then mockMvc.perform(patch("/api/v1/profile/supporter/me") @@ -93,7 +78,6 @@ void updateSupporterProfile() throws Exception { fieldWithPath("technicalTags.[]").type(ARRAY).description("변경할 기술 태그 목록") ), responseHeaders(headerWithName(LOCATION).description("redirect uri")) - )) - .andDo(print()); + )); } } diff --git a/backend/baton/src/test/java/touch/baton/document/runnerpost/create/RunnerPostApplicantApiTest.java b/backend/baton/src/test/java/touch/baton/document/runnerpost/create/RunnerPostApplicantApiTest.java index 9d00fb42a..6986a0e26 100644 --- a/backend/baton/src/test/java/touch/baton/document/runnerpost/create/RunnerPostApplicantApiTest.java +++ b/backend/baton/src/test/java/touch/baton/document/runnerpost/create/RunnerPostApplicantApiTest.java @@ -1,19 +1,14 @@ package touch.baton.document.runnerpost.create; -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.runner.Runner; -import touch.baton.domain.runnerpost.RunnerPost; -import touch.baton.domain.runnerpost.controller.RunnerPostController; -import touch.baton.domain.runnerpost.service.RunnerPostService; -import touch.baton.domain.runnerpost.service.dto.RunnerPostApplicantCreateRequest; -import touch.baton.domain.runnerpost.vo.Deadline; -import touch.baton.domain.supporter.Supporter; -import touch.baton.domain.tag.Tag; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostApplicantCreateRequest; +import touch.baton.domain.runnerpost.command.vo.Deadline; +import touch.baton.domain.tag.command.Tag; import touch.baton.fixture.domain.MemberFixture; import touch.baton.fixture.domain.RunnerFixture; import touch.baton.fixture.domain.RunnerPostFixture; @@ -27,33 +22,26 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; -import static org.springframework.http.HttpHeaders.*; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.HttpHeaders.LOCATION; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; -import static org.springframework.restdocs.headers.HeaderDocumentation.*; +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.restdocs.payload.JsonFieldType.STRING; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; 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(RunnerPostController.class) class RunnerPostApplicantApiTest extends RestdocsConfig { - @MockBean - private RunnerPostService runnerPostService; - - @BeforeEach - void setUp() { - final RunnerPostController runnerPostController = new RunnerPostController(runnerPostService); - restdocsSetUp(runnerPostController); - } - @DisplayName("Supporter 가 RunnerPost 에 리뷰를 제안한다.") @Test void createRunnerPostApplicant() throws Exception { @@ -68,9 +56,9 @@ void createRunnerPostApplicant() throws Exception { // when final RunnerPost spyRunnerPost = spy(runnerPost); when(spyRunnerPost.getId()).thenReturn(1L); - when(runnerPostService.createRunnerPostApplicant(any(), any(), any())).thenReturn(1L); + when(runnerPostCommandService.createRunnerPostApplicant(any(), any(), any())).thenReturn(1L); - when(oauthSupporterRepository.joinByMemberSocialId(any())).thenReturn(Optional.ofNullable(supporterHyena)); + when(oauthSupporterCommandRepository.joinByMemberSocialId(any())).thenReturn(Optional.ofNullable(supporterHyena)); final String token = getAccessTokenBySocialId(supporterHyena.getMember().getSocialId().getValue()); @@ -97,7 +85,6 @@ void createRunnerPostApplicant() throws Exception { responseHeaders( headerWithName(LOCATION).description("redirect uri") ) - )) - .andDo(print()); + )); } } 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 f42f82783..ef036474b 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 @@ -1,16 +1,11 @@ package touch.baton.document.runnerpost.create; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import touch.baton.config.RestdocsConfig; -import touch.baton.domain.member.Member; -import touch.baton.domain.runner.Runner; -import touch.baton.domain.runnerpost.controller.RunnerPostController; -import touch.baton.domain.runnerpost.service.RunnerPostService; -import touch.baton.domain.runnerpost.service.dto.RunnerPostCreateRequest; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostCreateRequest; import touch.baton.fixture.domain.MemberFixture; import touch.baton.fixture.domain.RunnerFixture; @@ -18,27 +13,22 @@ import java.util.List; import java.util.Optional; -import static org.apache.http.HttpHeaders.*; +import static org.apache.http.HttpHeaders.AUTHORIZATION; +import static org.apache.http.HttpHeaders.CONTENT_TYPE; +import static org.apache.http.HttpHeaders.LOCATION; import static org.mockito.BDDMockito.any; import static org.mockito.BDDMockito.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.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(RunnerPostController.class) class RunnerPostCreateApiTest extends RestdocsConfig { - @MockBean - private RunnerPostService runnerPostService; - - @BeforeEach - void setUp() { - restdocsSetUp(new RunnerPostController(runnerPostService)); - } - @DisplayName("러너 게시글 등록 API") @Test void createRunnerPost() throws Exception { @@ -58,8 +48,8 @@ void createRunnerPost() throws Exception { ); // when - when(runnerPostService.createRunnerPost(any(Runner.class), any(RunnerPostCreateRequest.class))).thenReturn(1L); - when(oauthRunnerRepository.joinByMemberSocialId(any())).thenReturn(Optional.ofNullable(runner)); + when(runnerPostCommandService.createRunnerPost(any(Runner.class), any(RunnerPostCreateRequest.class))).thenReturn(1L); + when(oauthRunnerCommandRepository.joinByMemberSocialId(any())).thenReturn(Optional.ofNullable(runner)); // then mockMvc.perform(post("/api/v1/posts/runner") diff --git a/backend/baton/src/test/java/touch/baton/document/runnerpost/delete/RunnerPostDeleteApiTest.java b/backend/baton/src/test/java/touch/baton/document/runnerpost/delete/RunnerPostDeleteApiTest.java index c2d34ed11..0df5cd56a 100644 --- a/backend/baton/src/test/java/touch/baton/document/runnerpost/delete/RunnerPostDeleteApiTest.java +++ b/backend/baton/src/test/java/touch/baton/document/runnerpost/delete/RunnerPostDeleteApiTest.java @@ -1,15 +1,10 @@ package touch.baton.document.runnerpost.delete; -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.runner.Runner; -import touch.baton.domain.runnerpost.RunnerPost; -import touch.baton.domain.runnerpost.controller.RunnerPostController; -import touch.baton.domain.runnerpost.service.RunnerPostService; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.runnerpost.command.RunnerPost; import touch.baton.fixture.domain.MemberFixture; import touch.baton.fixture.domain.RunnerFixture; import touch.baton.fixture.domain.RunnerPostFixture; @@ -29,17 +24,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static touch.baton.fixture.vo.DeadlineFixture.deadline; -@WebMvcTest(RunnerPostController.class) -public class RunnerPostDeleteApiTest extends RestdocsConfig { - - @MockBean - private RunnerPostService runnerPostService; - - @BeforeEach - void setUp() { - final RunnerPostController runnerPostController = new RunnerPostController(runnerPostService); - restdocsSetUp(runnerPostController); - } +class RunnerPostDeleteApiTest extends RestdocsConfig { @DisplayName("러너 게시글 삭제 API") @Test @@ -52,7 +37,7 @@ void deleteByRunnerPostId() throws Exception { final RunnerPost spyRunnerPost = spy(runnerPost); // when - when(oauthRunnerRepository.joinByMemberSocialId(any())).thenReturn(Optional.ofNullable(runner)); + when(oauthRunnerCommandRepository.joinByMemberSocialId(any())).thenReturn(Optional.ofNullable(runner)); when(spyRunnerPost.getId()).thenReturn(1L); // then diff --git a/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostCountOfSupporterByGuestApiTest.java b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostCountOfSupporterByGuestApiTest.java new file mode 100644 index 000000000..641c06d1f --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostCountOfSupporterByGuestApiTest.java @@ -0,0 +1,72 @@ +package touch.baton.document.runnerpost.read; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import touch.baton.config.RestdocsConfig; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.Deadline; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.RunnerFixture; +import touch.baton.fixture.domain.RunnerPostFixture; +import touch.baton.fixture.domain.SupporterFixture; +import touch.baton.fixture.domain.SupporterRunnerPostFixture; + +import java.time.LocalDateTime; + +import static org.mockito.ArgumentMatchers.eq; +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.NUMBER; +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; + +class RunnerPostCountOfSupporterByGuestApiTest extends RestdocsConfig { + + @DisplayName("서포터가 완료한 러너 게시글 개수 조회 API") + @Test + void countRunnerPostBySupporterIdAndReviewStatus() throws Exception { + // given + final Runner runner = RunnerFixture.createRunner(MemberFixture.createHyena()); + final Deadline deadline = deadline(LocalDateTime.now().plusHours(100)); + final RunnerPost runnerPost = RunnerPostFixture.create(runner, deadline); + + final Supporter supporter = SupporterFixture.create(MemberFixture.createDitoo()); + SupporterRunnerPostFixture.create(runnerPost, supporter); + runnerPost.assignSupporter(supporter); + + final Supporter spySupporter = spy(supporter); + given(spySupporter.getId()).willReturn(1L); + + // when + final long expectedCount = 1L; + when(runnerPostQueryService.countRunnerPostBySupporterIdAndReviewStatus(eq(1L), eq(ReviewStatus.DONE))) + .thenReturn(expectedCount); + + // then + mockMvc.perform(get("/api/v1/posts/runner/search/count") + .queryParam( + "supporterId", String.valueOf(spySupporter.getId()) + )) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andDo(restDocs.document( + queryParameters( + parameterWithName("supporterId").description("서포터 식별자 값") + ), + responseFields( + fieldWithPath("count").type(NUMBER).optional().description("게시글 개수") + )) + ); + } +} diff --git a/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostCountWithLoginedRunnerApiTest.java b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostCountWithLoginedRunnerApiTest.java new file mode 100644 index 000000000..6782990c3 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostCountWithLoginedRunnerApiTest.java @@ -0,0 +1,80 @@ +package touch.baton.document.runnerpost.read; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import touch.baton.config.RestdocsConfig; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.runnerpost.command.vo.Deadline; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.tag.command.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.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.MediaType.APPLICATION_JSON; +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.NUMBER; +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; + +class RunnerPostCountWithLoginedRunnerApiTest extends RestdocsConfig { + + @DisplayName("로그인한 러너와 관련된 러너 게시글 개수 조회 API") + @Test + void countRunnerPostByLoginedRunnerAndReviewStatus() throws Exception { + // given + final String socialId = "ditooSocialId"; + final Member loginedMember = MemberFixture.createWithSocialId(socialId); + final Runner loginedRunner = RunnerFixture.createRunner(loginedMember); + final String token = getAccessTokenBySocialId(socialId); + + final Tag javaTag = TagFixture.create(tagName("자바")); + final Deadline deadline = deadline(LocalDateTime.now().plusHours(100)); + RunnerPostFixture.create(loginedRunner, deadline, List.of(javaTag)); + final Runner spyLoginedRunner = spy(loginedRunner); + given(oauthRunnerCommandRepository.joinByMemberSocialId(any())).willReturn(Optional.ofNullable(spyLoginedRunner)); + + // when + final long expectedCount = 1L; + when(runnerPostQueryService.countRunnerPostByRunnerIdAndReviewStatus(eq(1L), eq(ReviewStatus.NOT_STARTED))) + .thenReturn(expectedCount); + + // then + mockMvc.perform(get("/api/v1/posts/runner/me/runner/count") + .header(AUTHORIZATION, "Bearer " + token) + .queryParam("reviewStatus", ReviewStatus.NOT_STARTED.name())) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andDo(restDocs.document( + requestHeaders( + headerWithName(AUTHORIZATION).description("Bearer JWT") + ), + queryParameters( + parameterWithName("reviewStatus").description("리뷰 상태") + ), + responseFields( + fieldWithPath("count").type(NUMBER).optional().description("게시글 개수") + )) + ); + } +} diff --git a/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostCountWithLoginedSupporterApiTest.java b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostCountWithLoginedSupporterApiTest.java new file mode 100644 index 000000000..f607b1508 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostCountWithLoginedSupporterApiTest.java @@ -0,0 +1,84 @@ +package touch.baton.document.runnerpost.read; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import touch.baton.config.RestdocsConfig; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.Deadline; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +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.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.MediaType.APPLICATION_JSON; +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.NUMBER; +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; + +class RunnerPostCountWithLoginedSupporterApiTest extends RestdocsConfig { + + @DisplayName("로그인한 서포터와 관련된 러너 게시글 개수 조회 API") + @Test + void countRunnerPostByLoginedSupporterAndReviewStatus() throws Exception { + // given + final String socialId = "ditooSocialId"; + final Member loginedMember = MemberFixture.createWithSocialId(socialId); + final Supporter loginedSupporter = SupporterFixture.create(loginedMember); + final String token = getAccessTokenBySocialId(socialId); + + final Runner runner = RunnerFixture.createRunner(MemberFixture.createHyena()); + + final Deadline deadline = deadline(LocalDateTime.now().plusHours(100)); + final RunnerPost runnerPost = RunnerPostFixture.create(runner, deadline); + SupporterRunnerPostFixture.create(runnerPost, loginedSupporter); + runnerPost.assignSupporter(loginedSupporter); + + final Supporter spyLoginedSupporter = spy(loginedSupporter); + given(oauthSupporterCommandRepository.joinByMemberSocialId(any())).willReturn(Optional.ofNullable(spyLoginedSupporter)); + + // when + final long expectedCount = 1L; + when(runnerPostQueryService.countRunnerPostBySupporterIdAndReviewStatus(eq(1L), eq(ReviewStatus.NOT_STARTED))) + .thenReturn(expectedCount); + + // then + mockMvc.perform(get("/api/v1/posts/runner/me/supporter/count") + .header(AUTHORIZATION, "Bearer " + token) + .queryParam("reviewStatus", ReviewStatus.NOT_STARTED.name())) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andDo(restDocs.document( + requestHeaders( + headerWithName(AUTHORIZATION).description("Bearer JWT") + ), + queryParameters( + parameterWithName("reviewStatus").description("리뷰 상태") + ), + responseFields( + fieldWithPath("count").type(NUMBER).optional().description("게시글 개수") + )) + ); + } +} diff --git a/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadOfSupporterByGuestApiTest.java b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadOfSupporterByGuestApiTest.java index 6067176c0..bdc9f55aa 100644 --- a/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadOfSupporterByGuestApiTest.java +++ b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadOfSupporterByGuestApiTest.java @@ -1,119 +1,103 @@ 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 touch.baton.config.RestdocsConfig; -import touch.baton.domain.runner.Runner; -import touch.baton.domain.runnerpost.RunnerPost; -import touch.baton.domain.runnerpost.controller.RunnerPostController; -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.supporter.Supporter; -import touch.baton.domain.tag.Tag; +import touch.baton.domain.common.request.PageParams; +import touch.baton.domain.common.response.PageResponse; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.Deadline; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.query.controller.response.RunnerPostResponse; +import touch.baton.domain.tag.command.Tag; 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.TagFixture; import java.time.LocalDateTime; import java.util.List; -import static java.nio.charset.StandardCharsets.UTF_8; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +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.*; +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.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.ReviewCountFixture.reviewCount; import static touch.baton.fixture.vo.TagNameFixture.tagName; -@WebMvcTest(RunnerPostController.class) class RunnerPostReadOfSupporterByGuestApiTest extends RestdocsConfig { - @MockBean - private RunnerPostService runnerPostService; - - @BeforeEach - void setUp() { - final RunnerPostController runnerPostController = new RunnerPostController(runnerPostService); - restdocsSetUp(runnerPostController); - } - @DisplayName("서포터와 연관된 러너 게시글 페이징 조회 API") @Test - void readReferencedBySupporter() throws Exception { + void readRunnerPostBySupporterIdAndReviewStatus() throws Exception { // given - final Runner runnerJudy = RunnerFixture.createRunner(MemberFixture.createJudy()); - final Supporter supporterHyena = SupporterFixture.create(reviewCount(10), MemberFixture.createHyena(), List.of(createJava(), createSpring())); + final Runner runner = RunnerFixture.createRunner(MemberFixture.createDitoo()); + final Supporter supporter = SupporterFixture.create(MemberFixture.createHyena()); final Tag javaTag = TagFixture.create(tagName("자바")); final Deadline deadline = deadline(LocalDateTime.now().plusHours(100)); - final RunnerPost runnerPost = RunnerPostFixture.create(runnerJudy, deadline, List.of(javaTag)); - runnerPost.assignSupporter(supporterHyena); + final RunnerPost runnerPost = RunnerPostFixture.create(runner, deadline, List.of(javaTag)); + runnerPost.assignSupporter(supporter); - // when + final Supporter spySupporter = spy(supporter); + given(spySupporter.getId()).willReturn(1L); final RunnerPost spyRunnerPost = spy(runnerPost); - final Supporter spySupporterHyena = spy(supporterHyena); - when(spySupporterHyena.getId()).thenReturn(1L); - when(spyRunnerPost.getId()).thenReturn(1L); + given(spyRunnerPost.getId()).willReturn(1L); - final List runnerPosts = List.of(spyRunnerPost); - final PageRequest pageOne = PageRequest.of(1, 10); - final PageImpl pageRunnerPosts = new PageImpl<>(runnerPosts, pageOne, runnerPosts.size()); - when(runnerPostService.readRunnerPostsBySupporterIdAndReviewStatus(any(), any(), any())) - .thenReturn(pageRunnerPosts); - when(runnerPostService.readCountsByRunnerPostIds(anyList())).thenReturn(List.of(1L)); + // when + final List responses = List.of(RunnerPostResponse.Simple.of( + spyRunnerPost, + 0L, + List.of(RunnerPostTagFixture.create(spyRunnerPost, javaTag)) + )); + final PageResponse pageResponse = PageResponse.of(responses, new PageParams(2L, 10)); + when(runnerPostQueryService.pageRunnerPostBySupporterIdAndReviewStatus(any(PageParams.class), anyLong(), any(ReviewStatus.class))) + .thenReturn(pageResponse); // then mockMvc.perform(get("/api/v1/posts/runner/search") - .characterEncoding(UTF_8) - .accept(APPLICATION_JSON) - .queryParam("size", String.valueOf(pageOne.getPageSize())) - .queryParam("page", String.valueOf(pageOne.getPageNumber())) - .queryParam("supporterId", String.valueOf(spySupporterHyena.getId())) + .queryParam("cursor", String.valueOf(1000L)) + .queryParam("limit", String.valueOf(10)) + .queryParam("supporterId", String.valueOf(spySupporter.getId())) .queryParam("reviewStatus", ReviewStatus.IN_PROGRESS.name())) .andExpect(status().isOk()) .andExpect(content().contentType(APPLICATION_JSON)) .andDo(restDocs.document( queryParameters( - parameterWithName("size").description("페이지 사이즈"), - parameterWithName("page").description("페이지 번호"), + parameterWithName("cursor").description("(Optional) 이전 페이지 마지막 게시글 식별자값(id)"), + parameterWithName("limit").description("페이지 사이즈"), parameterWithName("supporterId").description("서포터 식별자값"), - parameterWithName("reviewStatus").description("리뷰 상태") + parameterWithName("reviewStatus").description("(Optional) 리뷰 상태") ), responseFields( - 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("현재 페이지 데이터 수"), fieldWithPath("data.[].runnerPostId").type(NUMBER).description("러너 게시글 식별자값(id)"), - fieldWithPath("data.[].title").type(STRING).description("러너 게시글 제목"), + fieldWithPath("data.[].title").type(STRING).description("러너 게시글의 제목"), fieldWithPath("data.[].deadline").type(STRING).description("러너 게시글의 마감 기한"), - fieldWithPath("data.[].tags").type(ARRAY).description("러너 게시글 태그 목록"), fieldWithPath("data.[].watchedCount").type(NUMBER).description("러너 게시글의 조회수"), fieldWithPath("data.[].applicantCount").type(NUMBER).description("러너 게시글에 신청한 서포터 수"), - fieldWithPath("data.[].reviewStatus").type(STRING).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.isLast").type(BOOLEAN).description("마지막 페이지 여부"), + fieldWithPath("pageInfo.nextCursor").type(NUMBER).optional().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 5dd15fe5f..868ac7384 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 @@ -3,16 +3,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import touch.baton.config.RestdocsConfig; -import touch.baton.domain.member.Member; -import touch.baton.domain.runner.Runner; -import touch.baton.domain.runnerpost.RunnerPost; -import touch.baton.domain.runnerpost.controller.RunnerPostController; -import touch.baton.domain.runnerpost.service.RunnerPostService; -import touch.baton.domain.runnerpost.vo.Deadline; -import touch.baton.domain.tag.Tag; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.Deadline; +import touch.baton.domain.runnerpost.query.controller.RunnerPostQueryController; +import touch.baton.domain.tag.command.Tag; import touch.baton.fixture.domain.MemberFixture; import touch.baton.fixture.domain.RunnerFixture; import touch.baton.fixture.domain.RunnerPostFixture; @@ -30,7 +27,10 @@ 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.*; +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; @@ -40,18 +40,8 @@ import static touch.baton.fixture.vo.DeadlineFixture.deadline; import static touch.baton.fixture.vo.TagNameFixture.tagName; -@WebMvcTest(RunnerPostController.class) class RunnerPostReadOneApiTest extends RestdocsConfig { - @MockBean - private RunnerPostService runnerPostService; - - @BeforeEach - void setUp() { - final RunnerPostController runnerPostController = new RunnerPostController(runnerPostService); - restdocsSetUp(runnerPostController); - } - @DisplayName("러너 게시글 상세 조회 API") @Test void readByRunnerPostId() throws Exception { @@ -72,14 +62,14 @@ void readByRunnerPostId() throws Exception { final RunnerPost spyRunnerPost = spy(runnerPost); when(spyRunnerPost.getId()).thenReturn(1L); - when(runnerPostService.readByRunnerPostId(any())) + when(runnerPostQueryService.readByRunnerPostId(any())) .thenReturn(spyRunnerPost); - when(runnerPostService.readCountByRunnerPostId(any())) + when(runnerPostQueryService.countApplicantsByRunnerPostId(any())) .thenReturn(3L); final String token = getAccessTokenBySocialId(memberHyena.getSocialId().getValue()); - when(oauthMemberRepository.findBySocialId(any())) + when(oauthMemberCommandRepository.findBySocialId(any())) .thenReturn(Optional.ofNullable(memberHyena)); // then 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 index da8d9975e..7c272e67c 100644 --- 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 @@ -1,34 +1,26 @@ 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.domain.common.request.PageParams; +import touch.baton.domain.common.response.PageResponse; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.Deadline; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.query.controller.response.RunnerPostResponse; +import touch.baton.domain.tag.command.Tag; 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.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; @@ -48,21 +40,8 @@ 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 { @@ -78,29 +57,29 @@ void readRunnerPostsByTagNamesAndReviewStatus() throws Exception { 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))); + final List responses = List.of(RunnerPostResponse.Simple.of( + spyRunnerPost, + 0L, + List.of(RunnerPostTagFixture.create(spyRunnerPost, javaTag), RunnerPostTagFixture.create(spyRunnerPost, springTag)) + )); + final PageResponse pageResponse = PageResponse.of(responses, new PageParams(2L, 10)); + when(runnerPostQueryService.pageRunnerPostByTagNameAndReviewStatus(anyString(), any(PageParams.class), any(ReviewStatus.class))) + .thenReturn(pageResponse); // 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())) + mockMvc.perform(get("/api/v1/posts/runner") + .queryParam("tagName", javaTag.getTagName().getValue()) + .queryParam("cursor", String.valueOf(1000L)) + .queryParam("limit", String.valueOf(10)) + .queryParam("reviewStatus", ReviewStatus.NOT_STARTED.name())) .andExpect(status().isOk()) .andExpect(content().contentType(APPLICATION_JSON)) .andDo(restDocs.document( queryParameters( - parameterWithName("size").description("페이지 사이즈"), - parameterWithName("page").description("페이지 번호"), - parameterWithName("reviewStatus").description("리뷰 상태"), - parameterWithName("tagName").description("태그 이름") + parameterWithName("cursor").description("(Optional) 이전 페이지 마지막 게시글 식별자값(id)"), + parameterWithName("limit").description("페이지 사이즈"), + parameterWithName("reviewStatus").description("(Optional) 리뷰 상태"), + parameterWithName("tagName").description("(Optional) 태그 이름") ), responseFields( fieldWithPath("data.[].runnerPostId").type(NUMBER).description("러너 게시글 식별자값(id)"), @@ -112,13 +91,8 @@ void readRunnerPostsByTagNamesAndReviewStatus() throws Exception { 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("현재 페이지 데이터 수") + fieldWithPath("pageInfo.isLast").type(BOOLEAN).description("마지막 페이지 여부"), + fieldWithPath("pageInfo.nextCursor").type(NUMBER).optional().description("다음 커서") )) ); } diff --git a/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadWithLoginedRunnerApiTest.java b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadWithLoginedRunnerApiTest.java new file mode 100644 index 000000000..ea878765a --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadWithLoginedRunnerApiTest.java @@ -0,0 +1,115 @@ +package touch.baton.document.runnerpost.read; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import touch.baton.config.RestdocsConfig; +import touch.baton.domain.common.request.PageParams; +import touch.baton.domain.common.response.PageResponse; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.Deadline; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.query.controller.response.RunnerPostResponse; +import touch.baton.domain.tag.command.Tag; +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.TagFixture; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.MediaType.APPLICATION_JSON; +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.VARIES; +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; + +class RunnerPostReadWithLoginedRunnerApiTest extends RestdocsConfig { + + @DisplayName("로그인한 러너가 작성한 러너 게시글 페이징 조회 API") + @Test + void readRunnerPostByLoginedRunnerAndReviewStatus() throws Exception { + // given + final String socialId = "ditooSocialId"; + final Member loginedMember = MemberFixture.createWithSocialId(socialId); + final Runner loginedRunner = RunnerFixture.createRunner(loginedMember); + final String token = getAccessTokenBySocialId(socialId); + + final Tag javaTag = TagFixture.create(tagName("자바")); + final Deadline deadline = deadline(LocalDateTime.now().plusHours(100)); + final RunnerPost runnerPost = RunnerPostFixture.create(loginedRunner, deadline, List.of(javaTag)); + + final Runner spyLoginedRunner = spy(loginedRunner); + given(oauthRunnerCommandRepository.joinByMemberSocialId(any())).willReturn(Optional.ofNullable(spyLoginedRunner)); + given(Objects.requireNonNull(spyLoginedRunner).getId()).willReturn(1L); + final RunnerPost spyRunnerPost = spy(runnerPost); + given(spyRunnerPost.getId()).willReturn(1L); + + // when + final List responses = List.of(RunnerPostResponse.SimpleByRunner.of( + spyRunnerPost, + 0L, + List.of(RunnerPostTagFixture.create(spyRunnerPost, javaTag)) + )); + final PageResponse pageResponse = PageResponse.of(responses, new PageParams(2L, 10)); + when(runnerPostQueryService.pageRunnerPostByRunnerIdAndReviewStatus(any(PageParams.class), eq(1L), eq(ReviewStatus.NOT_STARTED))) + .thenReturn(pageResponse); + + // then + mockMvc.perform(get("/api/v1/posts/runner/me/runner") + .header(AUTHORIZATION, "Bearer " + token) + .queryParam("cursor", String.valueOf(1000L)) + .queryParam("limit", String.valueOf(10)) + .queryParam("reviewStatus", ReviewStatus.NOT_STARTED.name())) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andDo(restDocs.document( + requestHeaders( + headerWithName(AUTHORIZATION).description("Bearer JWT") + ), + queryParameters( + parameterWithName("cursor").description("(Optional) 이전 페이지 마지막 게시글 식별자값(id)"), + parameterWithName("limit").description("페이지 사이즈"), + parameterWithName("reviewStatus").description("(Optional) 리뷰 상태") + ), + 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.[].isReviewed").type(BOOLEAN).description("러너 게시글의 리뷰 완료 여부"), + fieldWithPath("data.[].supporterId").type(VARIES) + .optional() + .description("서포터 id (서포터가 존재할 때 NUMBER, 아닌 경우에 NULL)"), + fieldWithPath("data.[].tags.[]").type(ARRAY).description("러너 게시글의 태그 목록"), + fieldWithPath("pageInfo.isLast").type(BOOLEAN).description("마지막 페이지 여부"), + fieldWithPath("pageInfo.nextCursor").type(NUMBER).optional().description("다음 커서") + )) + ); + } +} diff --git a/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadWithLoginedSupporterApiTest.java b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadWithLoginedSupporterApiTest.java index 9df156ea0..ed7f0194f 100644 --- a/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadWithLoginedSupporterApiTest.java +++ b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadWithLoginedSupporterApiTest.java @@ -1,34 +1,33 @@ 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 touch.baton.config.RestdocsConfig; -import touch.baton.domain.member.Member; -import touch.baton.domain.runner.Runner; -import touch.baton.domain.runnerpost.RunnerPost; -import touch.baton.domain.runnerpost.controller.RunnerPostController; -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.supporter.Supporter; -import touch.baton.domain.tag.Tag; +import touch.baton.domain.common.request.PageParams; +import touch.baton.domain.common.response.PageResponse; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.Deadline; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.query.controller.response.RunnerPostResponse; +import touch.baton.domain.tag.command.Tag; 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.TagFixture; import java.time.LocalDateTime; import java.util.List; +import java.util.Objects; import java.util.Optional; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import static org.springframework.http.HttpHeaders.AUTHORIZATION; @@ -46,67 +45,48 @@ 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.CompanyFixture.company; import static touch.baton.fixture.vo.DeadlineFixture.deadline; -import static touch.baton.fixture.vo.GithubUrlFixture.githubUrl; -import static touch.baton.fixture.vo.ImageUrlFixture.imageUrl; -import static touch.baton.fixture.vo.MemberNameFixture.memberName; -import static touch.baton.fixture.vo.OauthIdFixture.oauthId; -import static touch.baton.fixture.vo.SocialIdFixture.socialId; import static touch.baton.fixture.vo.TagNameFixture.tagName; -@WebMvcTest(RunnerPostController.class) -public class RunnerPostReadWithLoginedSupporterApiTest extends RestdocsConfig { - - @MockBean - private RunnerPostService runnerPostService; - - @BeforeEach - void setUp() { - final RunnerPostController runnerPostController = new RunnerPostController(runnerPostService); - restdocsSetUp(runnerPostController); - } +class RunnerPostReadWithLoginedSupporterApiTest extends RestdocsConfig { @DisplayName("로그인한 서포터가 참여한 러너 게시글 페이징 조회 API") @Test - void readRunnerPostsByLoginedSupporterAndReviewStatus() throws Exception { + void readRunnerPostByLoginedSupporterAndReviewStatus() throws Exception { // given - final Runner runnerJudy = RunnerFixture.createRunner(MemberFixture.createJudy()); final String socialId = "ditooSocialId"; - final Member loginedMember = MemberFixture.create( - memberName("디투"), - socialId(socialId), - oauthId("abcd"), - githubUrl("naver.com"), - company("우아한테크코스"), - imageUrl("profile.jpg") - ); + final Member loginedMember = MemberFixture.createWithSocialId(socialId); final Supporter loginedSupporter = SupporterFixture.create(loginedMember); final String token = getAccessTokenBySocialId(socialId); + final Runner runner = RunnerFixture.createRunner(MemberFixture.createEthan()); + final Tag javaTag = TagFixture.create(tagName("자바")); final Deadline deadline = deadline(LocalDateTime.now().plusHours(100)); - final RunnerPost runnerPost = RunnerPostFixture.create(runnerJudy, deadline, List.of(javaTag)); + final RunnerPost runnerPost = RunnerPostFixture.create(runner, deadline, List.of(javaTag)); runnerPost.assignSupporter(loginedSupporter); - // when - final RunnerPost spyRunnerPost = spy(runnerPost); final Supporter spyLoginedSupporter = spy(loginedSupporter); - when(oauthSupporterRepository.joinByMemberSocialId(any())).thenReturn(Optional.ofNullable(spyLoginedSupporter)); - when(spyRunnerPost.getId()).thenReturn(1L); + given(oauthSupporterCommandRepository.joinByMemberSocialId(any())).willReturn(Optional.ofNullable(spyLoginedSupporter)); + given(Objects.requireNonNull(spyLoginedSupporter).getId()).willReturn(1L); + final RunnerPost spyRunnerPost = spy(runnerPost); + given(spyRunnerPost.getId()).willReturn(1L); - final List runnerPosts = List.of(spyRunnerPost); - final PageRequest pageOne = PageRequest.of(1, 10); - final PageImpl pageRunnerPosts = new PageImpl<>(runnerPosts, pageOne, runnerPosts.size()); - when(runnerPostService.readRunnerPostsBySupporterIdAndReviewStatus(any(), any(), any())) - .thenReturn(pageRunnerPosts); - when(runnerPostService.readCountsByRunnerPostIds(anyList())).thenReturn(List.of(1L)); + // when + final List responses = List.of(RunnerPostResponse.Simple.of( + spyRunnerPost, + 0L, + List.of(RunnerPostTagFixture.create(spyRunnerPost, javaTag)) + )); + final PageResponse pageResponse = PageResponse.of(responses, new PageParams(2L, 10)); + when(runnerPostQueryService.pageRunnerPostBySupporterIdAndReviewStatus(any(PageParams.class), eq(1L), eq(ReviewStatus.IN_PROGRESS))) + .thenReturn(pageResponse); // then mockMvc.perform(get("/api/v1/posts/runner/me/supporter") .header(AUTHORIZATION, "Bearer " + token) - .queryParam("size", String.valueOf(pageOne.getPageSize())) - .queryParam("page", String.valueOf(pageOne.getPageNumber())) + .queryParam("cursor", String.valueOf(1000L)) + .queryParam("limit", String.valueOf(10)) .queryParam("reviewStatus", ReviewStatus.IN_PROGRESS.name())) .andExpect(status().isOk()) .andExpect(content().contentType(APPLICATION_JSON)) @@ -115,25 +95,22 @@ void readRunnerPostsByLoginedSupporterAndReviewStatus() throws Exception { headerWithName(AUTHORIZATION).description("Bearer JWT") ), queryParameters( - parameterWithName("size").description("페이지 사이즈"), - parameterWithName("page").description("페이지 번호"), - parameterWithName("reviewStatus").description("리뷰 상태") + parameterWithName("cursor").description("(Optional) 이전 페이지 마지막 게시글 식별자값(id)"), + parameterWithName("limit").description("페이지 사이즈"), + parameterWithName("reviewStatus").description("(Optional) 리뷰 상태") ), responseFields( - 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("현재 페이지 데이터 수"), fieldWithPath("data.[].runnerPostId").type(NUMBER).description("러너 게시글 식별자값(id)"), - fieldWithPath("data.[].title").type(STRING).description("러너 게시글 제목"), + fieldWithPath("data.[].title").type(STRING).description("러너 게시글의 제목"), fieldWithPath("data.[].deadline").type(STRING).description("러너 게시글의 마감 기한"), - fieldWithPath("data.[].tags").type(ARRAY).description("러너 게시글 태그 목록"), fieldWithPath("data.[].watchedCount").type(NUMBER).description("러너 게시글의 조회수"), fieldWithPath("data.[].applicantCount").type(NUMBER).description("러너 게시글에 신청한 서포터 수"), - fieldWithPath("data.[].reviewStatus").type(STRING).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.isLast").type(BOOLEAN).description("마지막 페이지 여부"), + fieldWithPath("pageInfo.nextCursor").type(NUMBER).optional().description("다음 커서") )) ); } diff --git a/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostUpdateApplicantCancelationApiTest.java b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostUpdateApplicantCancelationApiTest.java index 876023abc..386e646a4 100644 --- a/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostUpdateApplicantCancelationApiTest.java +++ b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostUpdateApplicantCancelationApiTest.java @@ -1,18 +1,13 @@ 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.member.Member; -import touch.baton.domain.runner.Runner; -import touch.baton.domain.runnerpost.RunnerPost; -import touch.baton.domain.runnerpost.controller.RunnerPostController; -import touch.baton.domain.runnerpost.service.RunnerPostService; -import touch.baton.domain.runnerpost.vo.Deadline; -import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.Deadline; import touch.baton.fixture.domain.MemberFixture; import touch.baton.fixture.domain.RunnerFixture; import touch.baton.fixture.domain.RunnerPostFixture; @@ -42,17 +37,7 @@ import static touch.baton.fixture.vo.OauthIdFixture.oauthId; import static touch.baton.fixture.vo.SocialIdFixture.socialId; -@WebMvcTest(RunnerPostController.class) -public class RunnerPostUpdateApplicantCancelationApiTest extends RestdocsConfig { - - @MockBean - private RunnerPostService runnerPostService; - - @BeforeEach - void setUp() { - final RunnerPostController runnerPostController = new RunnerPostController(runnerPostService); - restdocsSetUp(runnerPostController); - } +class RunnerPostUpdateApplicantCancelationApiTest extends RestdocsConfig { @DisplayName("러너 게시글에 리뷰 제안한 서포터가 리뷰 제안 철회 API") @Test @@ -76,8 +61,8 @@ void updateSupporterCancelRunnerPost() throws Exception { // when when(spyRunnerPost.getId()).thenReturn(1L); - when(oauthSupporterRepository.joinByMemberSocialId(any())).thenReturn(Optional.ofNullable(supporter)); - runnerPostService.deleteSupporterRunnerPost(any(), eq(1L)); + when(oauthSupporterCommandRepository.joinByMemberSocialId(any())).thenReturn(Optional.ofNullable(supporter)); + runnerPostCommandService.deleteSupporterRunnerPost(any(), eq(1L)); // then mockMvc.perform(patch("/api/v1/posts/runner/{runnerPostId}/cancelation", 1L) diff --git a/backend/baton/src/test/java/touch/baton/document/runnerpost/read/SupporterRunnerPostReadApiTest.java b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/SupporterRunnerPostReadApiTest.java index c1ba8347d..30da50277 100644 --- a/backend/baton/src/test/java/touch/baton/document/runnerpost/read/SupporterRunnerPostReadApiTest.java +++ b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/SupporterRunnerPostReadApiTest.java @@ -1,20 +1,15 @@ 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.member.Member; -import touch.baton.domain.runner.Runner; -import touch.baton.domain.runnerpost.RunnerPost; -import touch.baton.domain.runnerpost.controller.RunnerPostController; -import touch.baton.domain.runnerpost.service.RunnerPostService; -import touch.baton.domain.runnerpost.vo.Deadline; -import touch.baton.domain.supporter.Supporter; -import touch.baton.domain.supporter.SupporterRunnerPost; -import touch.baton.domain.tag.Tag; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.SupporterRunnerPost; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.Deadline; +import touch.baton.domain.tag.command.Tag; import touch.baton.fixture.domain.MemberFixture; import touch.baton.fixture.domain.RunnerFixture; import touch.baton.fixture.domain.RunnerPostFixture; @@ -36,7 +31,9 @@ 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.*; +import static org.springframework.restdocs.payload.JsonFieldType.ARRAY; +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; @@ -46,18 +43,8 @@ import static touch.baton.fixture.vo.DeadlineFixture.deadline; import static touch.baton.fixture.vo.TagNameFixture.tagName; -@WebMvcTest(RunnerPostController.class) class SupporterRunnerPostReadApiTest extends RestdocsConfig { - @MockBean - private RunnerPostService runnerPostService; - - @BeforeEach - void setUp() { - final RunnerPostController runnerPostController = new RunnerPostController(runnerPostService); - restdocsSetUp(runnerPostController); - } - @DisplayName("러너 게시글의 지원한 서포터 목록 조회 API") @Test void readSupporterRunnerPostsByRunnerPostId() throws Exception { @@ -77,8 +64,8 @@ void readSupporterRunnerPostsByRunnerPostId() throws Exception { // when given(spySupporter.getId()).willReturn(1L); given(spyRunnerPost.getId()).willReturn(1L); - given(runnerPostService.readSupporterRunnerPostsByRunnerPostId(any(), any())).willReturn(List.of(supporterRunnerPost)); - when(oauthRunnerRepository.joinByMemberSocialId(notNull())) + given(runnerPostQueryService.readSupporterRunnerPostsByRunnerPostId(any(), any())).willReturn(List.of(supporterRunnerPost)); + when(oauthRunnerCommandRepository.joinByMemberSocialId(notNull())) .thenReturn(Optional.ofNullable(runner)); // then 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 index 721b370e6..18e65d9b5 100644 --- 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 @@ -1,20 +1,15 @@ 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.domain.tag.command.Tag; +import touch.baton.domain.tag.command.vo.TagReducedName; 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; @@ -29,18 +24,8 @@ 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 { @@ -51,7 +36,7 @@ void readTagsByReducedName() throws Exception { final Tag javascriptTagSpy = spy(javascriptTag); // when - when(tagService.readTagsByReducedName("java")) + when(tagQueryService.readTagsByReducedName(TagReducedName.nullableInstance("java"), 10)) .thenReturn(List.of(javaTagSpy, javascriptTagSpy)); when(javaTagSpy.getId()) .thenReturn(1L); diff --git a/backend/baton/src/test/java/touch/baton/document/runnerpost/update/RunnerPostUpdateApiTest.java b/backend/baton/src/test/java/touch/baton/document/runnerpost/update/RunnerPostUpdateApiTest.java index 7889c82d2..515600216 100644 --- a/backend/baton/src/test/java/touch/baton/document/runnerpost/update/RunnerPostUpdateApiTest.java +++ b/backend/baton/src/test/java/touch/baton/document/runnerpost/update/RunnerPostUpdateApiTest.java @@ -1,17 +1,12 @@ package touch.baton.document.runnerpost.update; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import touch.baton.config.RestdocsConfig; -import touch.baton.domain.member.Member; -import touch.baton.domain.runner.Runner; -import touch.baton.domain.runnerpost.controller.RunnerPostController; -import touch.baton.domain.runnerpost.service.RunnerPostService; -import touch.baton.domain.runnerpost.service.dto.RunnerPostUpdateRequest; -import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostUpdateRequest; import touch.baton.fixture.domain.MemberFixture; import touch.baton.fixture.domain.RunnerFixture; import touch.baton.fixture.domain.SupporterFixture; @@ -21,7 +16,7 @@ import static org.apache.http.HttpHeaders.CONTENT_TYPE; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.when; import static org.springframework.http.HttpHeaders.AUTHORIZATION; import static org.springframework.http.HttpHeaders.LOCATION; @@ -32,20 +27,10 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(RunnerPostController.class) -public class RunnerPostUpdateApiTest extends RestdocsConfig { - - @MockBean - private RunnerPostService runnerPostService; - - @BeforeEach - void setUp() { - restdocsSetUp(new RunnerPostController(runnerPostService)); - } +class RunnerPostUpdateApiTest extends RestdocsConfig { @DisplayName("제안한 서포터 목록 중에서 서포터로 선택하는 API") @Test @@ -59,8 +44,8 @@ void updateRunnerPostSupporter() throws Exception { final RunnerPostUpdateRequest.SelectSupporter request = new RunnerPostUpdateRequest.SelectSupporter(1L); // when - willDoNothing().given(runnerPostService).updateRunnerPostAppliedSupporter(any(Runner.class), anyLong(), any(RunnerPostUpdateRequest.SelectSupporter.class)); - when(oauthRunnerRepository.joinByMemberSocialId(any())).thenReturn(Optional.ofNullable(ditooRunner)); + doNothing().when(runnerPostCommandService).updateRunnerPostAppliedSupporter(any(Runner.class), anyLong(), any(RunnerPostUpdateRequest.SelectSupporter.class)); + when(oauthRunnerCommandRepository.joinByMemberSocialId(any())).thenReturn(Optional.ofNullable(ditooRunner)); // then mockMvc.perform(patch("/api/v1/posts/runner/{runnerPostId}/supporters", 1L) @@ -74,7 +59,7 @@ void updateRunnerPostSupporter() throws Exception { requestHeaders(headerWithName(AUTHORIZATION).description("Bearer JWT"), headerWithName(CONTENT_TYPE).description(APPLICATION_JSON_VALUE)), responseHeaders(headerWithName(LOCATION).description("Redirect URI")) - )).andDo(print()); + )); } @DisplayName("서포터 리뷰 완료 API") @@ -87,8 +72,8 @@ void updateRunnerPostReviewStatusDone() throws Exception { final String accessToken = getAccessTokenBySocialId(ditooSocialId); // when - willDoNothing().given(runnerPostService).updateRunnerPostReviewStatusDone(anyLong(), any(Supporter.class)); - when(oauthSupporterRepository.joinByMemberSocialId(any())).thenReturn(Optional.ofNullable(supporter)); + doNothing().when(runnerPostCommandService).updateRunnerPostReviewStatusDone(anyLong(), any(Supporter.class)); + when(oauthSupporterCommandRepository.joinByMemberSocialId(any())).thenReturn(Optional.ofNullable(supporter)); // then mockMvc.perform(patch("/api/v1/posts/runner/{runnerPostId}/done", 1L) diff --git a/backend/baton/src/test/java/touch/baton/domain/common/vo/DescriptionTest.java b/backend/baton/src/test/java/touch/baton/domain/common/vo/DescriptionTest.java index 88ec103e2..98583b0dd 100644 --- a/backend/baton/src/test/java/touch/baton/domain/common/vo/DescriptionTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/common/vo/DescriptionTest.java @@ -2,7 +2,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import touch.baton.domain.feedback.vo.Description; +import touch.baton.domain.feedback.command.vo.Description; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/backend/baton/src/test/java/touch/baton/domain/common/vo/TitleTest.java b/backend/baton/src/test/java/touch/baton/domain/common/vo/TitleTest.java index a27af7c9f..7c12839ff 100644 --- a/backend/baton/src/test/java/touch/baton/domain/common/vo/TitleTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/common/vo/TitleTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import touch.baton.domain.runnerpost.command.vo.Title; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/backend/baton/src/test/java/touch/baton/domain/common/vo/WatchedCountTest.java b/backend/baton/src/test/java/touch/baton/domain/common/vo/WatchedCountTest.java index 85542d55d..eeaef4a2f 100644 --- a/backend/baton/src/test/java/touch/baton/domain/common/vo/WatchedCountTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/common/vo/WatchedCountTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import touch.baton.domain.runnerpost.command.vo.WatchedCount; import static org.assertj.core.api.Assertions.assertThat; diff --git a/backend/baton/src/test/java/touch/baton/domain/feedback/repository/SupporterFeedbackCommandRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/feedback/repository/SupporterFeedbackCommandRepositoryTest.java new file mode 100644 index 000000000..1dd932980 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/feedback/repository/SupporterFeedbackCommandRepositoryTest.java @@ -0,0 +1,66 @@ +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.command.SupporterFeedback; +import touch.baton.domain.feedback.command.repository.SupporterFeedbackCommandRepository; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.runnerpost.command.RunnerPost; +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 SupporterFeedbackCommandRepositoryTest extends RepositoryTestConfig { + + @Autowired + private EntityManager em; + @Autowired + private SupporterFeedbackCommandRepository supporterFeedbackCommandRepository; + + @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(supporterFeedbackCommandRepository.existsByRunnerPostIdAndSupporterId(runnerPost.getId(), reviewedSupporter.getId())).isTrue(); + softly.assertThat(supporterFeedbackCommandRepository.existsByRunnerPostIdAndSupporterId(runnerPost.getId(), notReviewedSupporter.getId())).isFalse(); + }); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/feedback/service/FeedbackCommandServiceTest.java b/backend/baton/src/test/java/touch/baton/domain/feedback/service/FeedbackCommandServiceTest.java new file mode 100644 index 000000000..55713aeb9 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/feedback/service/FeedbackCommandServiceTest.java @@ -0,0 +1,94 @@ +package touch.baton.domain.feedback.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.feedback.command.service.FeedbackCommandService; +import touch.baton.domain.feedback.command.service.dto.SupporterFeedBackCreateRequest; +import touch.baton.domain.feedback.exception.FeedbackBusinessException; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.runnerpost.command.RunnerPost; +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.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class FeedbackCommandServiceTest extends ServiceTestConfig { + + private FeedbackCommandService feedbackCommandService; + private Runner exactRunner; + private RunnerPost runnerPost; + private SupporterFeedBackCreateRequest request; + private Supporter reviewedSupporter; + + @BeforeEach + void setUp() { + feedbackCommandService = new FeedbackCommandService(supporterFeedbackCommandRepository, runnerPostCommandRepository, supporterCommandRepository); + final Member ethan = memberCommandRepository.save(MemberFixture.createEthan()); + exactRunner = runnerQueryRepository.save(RunnerFixture.createRunner(ethan)); + final Member ditoo = memberCommandRepository.save(MemberFixture.createDitoo()); + reviewedSupporter = supporterQueryRepository.save(SupporterFixture.create(ditoo)); + runnerPost = runnerPostQueryRepository.save(RunnerPostFixture.create(exactRunner, reviewedSupporter)); + + request = new SupporterFeedBackCreateRequest("GOOD", List.of("코드리뷰가 맛있어요.", "말투가 친절해요."), reviewedSupporter.getId(), runnerPost.getId()); + } + + @DisplayName("러너가 서포터 피드백을 할 수 있다.") + @Test + void createSupporterFeedback() { + // when + final Long expected = feedbackCommandService.createSupporterFeedback(exactRunner, request); + + // then + assertSoftly(softly -> { + softly.assertThat(expected).isNotNull(); + softly.assertThat(runnerPost.getIsReviewed().getValue()).isTrue(); + }); + } + + @DisplayName("소유자가 아닌 러너는 피드백을 할 수 없다.") + @Test + void fail_createSupporterFeedback_if_not_owner_runner() { + // given + final Member differentMember = memberCommandRepository.save(MemberFixture.createHyena()); + final Runner notOwner = runnerQueryRepository.save(RunnerFixture.createRunner(differentMember)); + + // when, then + assertThatThrownBy(() -> feedbackCommandService.createSupporterFeedback(notOwner, request)) + .isInstanceOf(FeedbackBusinessException.class); + } + + @DisplayName("리뷰를 하지 않은 서포터를 피드백을 할 수 없다.") + @Test + void fail_createSupporterFeedback_if_not_review_supporter_runner() { + // given + final Member differentMember = memberCommandRepository.save(MemberFixture.createHyena()); + final Supporter notReviewSupporter = supporterQueryRepository.save(SupporterFixture.create(differentMember)); + final SupporterFeedBackCreateRequest notReviewSupporterRequest = new SupporterFeedBackCreateRequest("GOOD", new ArrayList<>(), notReviewSupporter.getId(), runnerPost.getId()); + + // when, then + assertThatThrownBy(() -> feedbackCommandService.createSupporterFeedback(exactRunner, notReviewSupporterRequest)) + .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()); + feedbackCommandService.createSupporterFeedback(exactRunner, supporterFeedBackCreateRequest); + + // when, then + assertThatThrownBy(() -> feedbackCommandService.createSupporterFeedback(exactRunner, supporterFeedBackCreateRequest)) + .isInstanceOf(FeedbackBusinessException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/member/MemberTest.java b/backend/baton/src/test/java/touch/baton/domain/member/MemberTest.java index a3ff8dbc1..2732dfb11 100644 --- a/backend/baton/src/test/java/touch/baton/domain/member/MemberTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/member/MemberTest.java @@ -4,18 +4,17 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.vo.Company; +import touch.baton.domain.member.command.vo.GithubUrl; +import touch.baton.domain.member.command.vo.ImageUrl; +import touch.baton.domain.member.command.vo.MemberName; +import touch.baton.domain.member.command.vo.OauthId; +import touch.baton.domain.member.command.vo.SocialId; import touch.baton.domain.member.exception.MemberDomainException; -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.fixture.domain.MemberFixture; -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.*; class MemberTest { @@ -37,19 +36,21 @@ void success() { ).doesNotThrowAnyException(); } - @DisplayName("이름에 null 이 들어갈 경우 예외가 발생한다.") + @DisplayName("이름에 null 이 들어갈 경우 기본값으로 생성한다.") @Test - void fail_if_name_is_null() { - assertThatThrownBy(() -> Member.builder() - .memberName(null) + void success_if_name_is_null_then_default_value() { + // given + final Member member = Member.builder() + .memberName(new MemberName(null)) .socialId(new SocialId("testSocialId")) .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) .githubUrl(new GithubUrl("github.com/hyena0608")) .company(new Company("우아한형제들")) .imageUrl(new ImageUrl("imageUrl")) - .build() - ).isInstanceOf(MemberDomainException.class) - .hasMessage("Member 의 name 은 null 일 수 없습니다."); + .build(); + + // when, then + assertThat(member.getMemberName()).isEqualTo(new MemberName("익명의 사용자")); } @DisplayName("socialId에 null 이 들어갈 경우 예외가 발생한다.") diff --git a/backend/baton/src/test/java/touch/baton/domain/member/vo/CompanyTest.java b/backend/baton/src/test/java/touch/baton/domain/member/vo/CompanyTest.java index e9312518e..9b6cb3b7c 100644 --- a/backend/baton/src/test/java/touch/baton/domain/member/vo/CompanyTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/member/vo/CompanyTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import touch.baton.domain.member.command.vo.Company; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/backend/baton/src/test/java/touch/baton/domain/member/vo/GithubUrlTest.java b/backend/baton/src/test/java/touch/baton/domain/member/vo/GithubUrlTest.java index 28c73ca0f..2bf35a995 100644 --- a/backend/baton/src/test/java/touch/baton/domain/member/vo/GithubUrlTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/member/vo/GithubUrlTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import touch.baton.domain.member.command.vo.GithubUrl; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/backend/baton/src/test/java/touch/baton/domain/member/vo/ImageUrlTest.java b/backend/baton/src/test/java/touch/baton/domain/member/vo/ImageUrlTest.java index 511c551c4..a03baa970 100644 --- a/backend/baton/src/test/java/touch/baton/domain/member/vo/ImageUrlTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/member/vo/ImageUrlTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import touch.baton.domain.member.command.vo.ImageUrl; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/backend/baton/src/test/java/touch/baton/domain/member/vo/MemberNameTest.java b/backend/baton/src/test/java/touch/baton/domain/member/vo/MemberNameTest.java new file mode 100644 index 000000000..fd5058eb7 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/member/vo/MemberNameTest.java @@ -0,0 +1,20 @@ +package touch.baton.domain.member.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import touch.baton.domain.member.command.vo.MemberName; + +import static org.assertj.core.api.Assertions.assertThat; + +class MemberNameTest { + + @DisplayName("value 가 null 이면 기본값으로_생성한다") + @Test + void fail_if_value_is_null_then_default_value() { + // given + final MemberName memberName = new MemberName(null); + + // when, then + assertThat(memberName.getValue()).isEqualTo("익명의 사용자"); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/member/vo/OauthIdTest.java b/backend/baton/src/test/java/touch/baton/domain/member/vo/OauthIdTest.java index 20582fcc6..9546692f7 100644 --- a/backend/baton/src/test/java/touch/baton/domain/member/vo/OauthIdTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/member/vo/OauthIdTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import touch.baton.domain.member.command.vo.OauthId; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/backend/baton/src/test/java/touch/baton/domain/member/vo/SocialIdTest.java b/backend/baton/src/test/java/touch/baton/domain/member/vo/SocialIdTest.java index 818617c06..2677e9886 100644 --- a/backend/baton/src/test/java/touch/baton/domain/member/vo/SocialIdTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/member/vo/SocialIdTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import touch.baton.domain.member.command.vo.SocialId; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/backend/baton/src/test/java/touch/baton/domain/notification/command/NotificationTest.java b/backend/baton/src/test/java/touch/baton/domain/notification/command/NotificationTest.java new file mode 100644 index 000000000..3b52e7a6a --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/notification/command/NotificationTest.java @@ -0,0 +1,207 @@ +package touch.baton.domain.notification.command; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.vo.Company; +import touch.baton.domain.member.command.vo.GithubUrl; +import touch.baton.domain.member.command.vo.ImageUrl; +import touch.baton.domain.member.command.vo.MemberName; +import touch.baton.domain.member.command.vo.OauthId; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.notification.command.vo.IsRead; +import touch.baton.domain.notification.command.vo.NotificationMessage; +import touch.baton.domain.notification.command.vo.NotificationReferencedId; +import touch.baton.domain.notification.command.vo.NotificationTitle; +import touch.baton.domain.notification.command.vo.NotificationType; +import touch.baton.domain.notification.exception.NotificationDomainException; + +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 NotificationTest { + + private static final Member owner = Member.builder() + .memberName(new MemberName("사용자 테스트용 이름")) + .socialId(new SocialId("사용자 테스트용 소셜 아이디")) + .oauthId(new OauthId("사용자 테스트용 오어스 아이디")) + .githubUrl(new GithubUrl("https://github.com/사용자_테스트용_깃허브_주소")) + .company(new Company("사용자 테스트용 회사명")) + .imageUrl(new ImageUrl("https://사용자_테스트용_이미지_주소")) + .build(); + + private static final Member notOwner = Member.builder() + .memberName(new MemberName("사용자 테스트용 이름")) + .socialId(new SocialId("사용자 테스트용 소셜 아이디")) + .oauthId(new OauthId("사용자 테스트용 오어스 아이디")) + .githubUrl(new GithubUrl("https://github.com/사용자_테스트용_깃허브_주소")) + .company(new Company("사용자 테스트용 회사명")) + .imageUrl(new ImageUrl("https://사용자_테스트용_이미지_주소")) + .build(); + + @DisplayName("생성 테스트") + @Nested + class Create { + + @DisplayName("성공한다") + @Test + void success() { + assertThatCode(() -> Notification.builder() + .notificationTitle(new NotificationTitle("알림 테스트용 제목")) + .notificationMessage(new NotificationMessage("알림 테스트용 내용")) + .notificationType(NotificationType.RUNNER_POST) + .notificationReferencedId(new NotificationReferencedId(1L)) + .isRead(IsRead.asUnRead()) + .member(owner) + .build() + ).doesNotThrowAnyException(); + } + + @DisplayName("notificationTitle 에 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_notificationTitle_is_null() { + assertThatThrownBy(() -> Notification.builder() + .notificationTitle(null) + .notificationMessage(new NotificationMessage("알림 테스트용 내용")) + .notificationType(NotificationType.RUNNER_POST) + .notificationReferencedId(new NotificationReferencedId(1L)) + .isRead(IsRead.asUnRead()) + .member(owner) + .build() + ).isInstanceOf(NotificationDomainException.class); + } + + @DisplayName("notificationMessage 에 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_notificationMessage_is_null() { + assertThatThrownBy(() -> Notification.builder() + .notificationTitle(new NotificationTitle("알림 테스트용 제목")) + .notificationMessage(null) + .notificationType(NotificationType.RUNNER_POST) + .notificationReferencedId(new NotificationReferencedId(1L)) + .isRead(IsRead.asUnRead()) + .member(owner) + .build() + ).isInstanceOf(NotificationDomainException.class); + } + + @DisplayName("notificationType() 에 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_notificationType_is_null() { + assertThatThrownBy(() -> Notification.builder() + .notificationTitle(new NotificationTitle("알림 테스트용 제목")) + .notificationMessage(new NotificationMessage("알림 테스트용 내용")) + .notificationType(null) + .notificationReferencedId(new NotificationReferencedId(1L)) + .isRead(IsRead.asUnRead()) + .member(owner) + .build() + ).isInstanceOf(NotificationDomainException.class); + } + + @DisplayName("notificationReferencedId() 에 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_notificationReferencedId_is_null() { + assertThatThrownBy(() -> Notification.builder() + .notificationTitle(new NotificationTitle("알림 테스트용 제목")) + .notificationMessage(new NotificationMessage("알림 테스트용 내용")) + .notificationType(NotificationType.RUNNER_POST) + .notificationReferencedId(null) + .isRead(IsRead.asUnRead()) + .member(owner) + .build() + ).isInstanceOf(NotificationDomainException.class); + } + + @DisplayName("isRead 에 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_isRead_is_null() { + assertThatThrownBy(() -> Notification.builder() + .notificationTitle(new NotificationTitle("알림 테스트용 제목")) + .notificationMessage(new NotificationMessage("알림 테스트용 내용")) + .notificationType(NotificationType.RUNNER_POST) + .notificationReferencedId(new NotificationReferencedId(1L)) + .isRead(null) + .member(owner) + .build() + ).isInstanceOf(NotificationDomainException.class); + } + + @DisplayName("member 에 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_member_is_null() { + assertThatThrownBy(() -> Notification.builder() + .notificationTitle(new NotificationTitle("알림 테스트용 제목")) + .notificationMessage(new NotificationMessage("알림 테스트용 내용")) + .notificationType(NotificationType.RUNNER_POST) + .notificationReferencedId(new NotificationReferencedId(1L)) + .isRead(IsRead.asUnRead()) + .member(null) + .build() + ).isInstanceOf(NotificationDomainException.class); + } + } + + @DisplayName("알림 여부를 수정할 때 주인(Member) 일 경우 읽음 상태로 수정할 수 있다.") + @Test + void success_markAsRead() { + // given + final Notification notification = Notification.builder() + .notificationTitle(new NotificationTitle("알림 테스트용 제목")) + .notificationMessage(new NotificationMessage("알림 테스트용 내용")) + .notificationType(NotificationType.RUNNER_POST) + .notificationReferencedId(new NotificationReferencedId(1L)) + .isRead(IsRead.asUnRead()) + .member(owner) + .build(); + + // when + notification.markAsRead(owner); + + // then + final boolean actual = notification.getIsRead().getValue(); + + assertThat(actual).isTrue(); + } + + @DisplayName("알림 여부를 수정할 때 주인(Member) 이 아닐 경우 예외가 발생한다.") + @Test + void fail_markAsRead_when_member_isNotOwner() { + // given + final Notification notification = Notification.builder() + .notificationTitle(new NotificationTitle("알림 테스트용 제목")) + .notificationMessage(new NotificationMessage("알림 테스트용 내용")) + .notificationType(NotificationType.RUNNER_POST) + .notificationReferencedId(new NotificationReferencedId(1L)) + .isRead(IsRead.asUnRead()) + .member(owner) + .build(); + + // when & then + assertThatThrownBy(() -> notification.markAsRead(notOwner)) + .isInstanceOf(NotificationDomainException.class); + } + + @DisplayName("알림의 주인(Member) 이 아닌지 확인한다.") + @Test + void isNotOwner() { + // given + final Notification notification = Notification.builder() + .notificationTitle(new NotificationTitle("알림 테스트용 제목")) + .notificationMessage(new NotificationMessage("알림 테스트용 내용")) + .notificationType(NotificationType.RUNNER_POST) + .notificationReferencedId(new NotificationReferencedId(1L)) + .isRead(IsRead.asUnRead()) + .member(owner) + .build(); + + // when & then + assertSoftly(softly -> { + softly.assertThat(notification.isNotOwner(owner)).isFalse(); + softly.assertThat(notification.isNotOwner(notOwner)).isTrue(); + }); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/notification/command/event/NotificationEventListenerTest.java b/backend/baton/src/test/java/touch/baton/domain/notification/command/event/NotificationEventListenerTest.java new file mode 100644 index 000000000..d88091d78 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/notification/command/event/NotificationEventListenerTest.java @@ -0,0 +1,148 @@ +package touch.baton.domain.notification.command.event; + +import org.junit.jupiter.api.BeforeEach; +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.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.notification.command.Notification; +import touch.baton.domain.notification.command.repository.NotificationCommandRepository; +import touch.baton.domain.notification.command.vo.NotificationType; +import touch.baton.domain.notification.query.repository.NotificationQuerydslRepository; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.event.RunnerPostApplySupporterEvent; +import touch.baton.domain.runnerpost.command.event.RunnerPostAssignSupporterEvent; +import touch.baton.domain.runnerpost.command.event.RunnerPostReviewStatusDoneEvent; +import touch.baton.domain.runnerpost.query.repository.RunnerPostQueryRepository; +import touch.baton.fixture.domain.MemberFixture; + +import java.util.List; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class NotificationEventListenerTest extends RepositoryTestConfig { + + private NotificationEventListener notificationEventListener; + + @Autowired + private NotificationQuerydslRepository notificationQuerydslRepository; + + @BeforeEach + void setUp(@Autowired NotificationCommandRepository notificationCommandRepository, + @Autowired RunnerPostQueryRepository runnerPostQueryRepository + ) { + notificationEventListener = new NotificationEventListener(notificationCommandRepository, runnerPostQueryRepository); + } + + @DisplayName("러너 게시글에 서포터가 지원했다는 알림을 생성한다.") + @Test + void subscribeRunnerPostApplySupporterEvent() { + // given + final Runner targetRunner = persistRunner(MemberFixture.createHyena()); + final RunnerPost runnerPost = persistRunnerPost(targetRunner); + + // when + final RunnerPostApplySupporterEvent event = new RunnerPostApplySupporterEvent(runnerPost.getId()); + notificationEventListener.subscribeRunnerPostApplySupporterEvent(event); + + // then + final List actualNotifications = notificationQuerydslRepository.findByMemberId(targetRunner.getMember().getId(), 10); + + final String expectedNotificationTitle = "서포터의 제안이 왔습니다."; + final String expectedNotificationMessage = String.format("관련 게시글 - %s", runnerPost.getTitle().getValue()); + final NotificationType expectedNotificationType = NotificationType.RUNNER_POST; + final Long expectedReferencedId = runnerPost.getId(); + final boolean expectedIsRead = false; + final Member expectedMember = targetRunner.getMember(); + + assertSoftly(softly -> { + softly.assertThat(actualNotifications).hasSize(1); + final Notification actual = actualNotifications.get(0); + + softly.assertThat(actual.getId()).isPositive(); + softly.assertThat(actual.getNotificationTitle().getValue()).isEqualTo(expectedNotificationTitle); + softly.assertThat(actual.getNotificationMessage().getValue()).isEqualTo(expectedNotificationMessage); + softly.assertThat(actual.getNotificationType()).isEqualTo(expectedNotificationType); + softly.assertThat(actual.getNotificationReferencedId().getValue()).isEqualTo(expectedReferencedId); + softly.assertThat(actual.getIsRead().getValue()).isEqualTo(expectedIsRead); + softly.assertThat(actual.getMember()).isEqualTo(expectedMember); + }); + } + + @DisplayName("러너 게시글 리뷰 상태가 DONE 으로 업데이트 되었다는 알림을 생성한다.") + @Test + void subscribeRunnerPostReviewStatusDoneEvent() { + // given + final Runner targetRunner = persistRunner(MemberFixture.createHyena()); + final RunnerPost runnerPost = persistRunnerPost(targetRunner); + + // when + final RunnerPostReviewStatusDoneEvent event = new RunnerPostReviewStatusDoneEvent(runnerPost.getId()); + notificationEventListener.subscribeRunnerPostReviewStatusDoneEvent(event); + + // then + final List actualNotifications = notificationQuerydslRepository.findByMemberId(targetRunner.getMember().getId(), 10); + + final String expectedNotificationTitle = "코드 리뷰 상태가 완료로 변경되었습니다."; + final String expectedNotificationMessage = String.format("관련 게시글 - %s", runnerPost.getTitle().getValue()); + final NotificationType expectedNotificationType = NotificationType.RUNNER_POST; + final Long expectedReferencedId = runnerPost.getId(); + final boolean expectedIsRead = false; + final Member expectedMember = targetRunner.getMember(); + + assertSoftly(softly -> { + softly.assertThat(actualNotifications).hasSize(1); + final Notification actual = actualNotifications.get(0); + + softly.assertThat(actual.getId()).isPositive(); + softly.assertThat(actual.getNotificationTitle().getValue()).isEqualTo(expectedNotificationTitle); + softly.assertThat(actual.getNotificationMessage().getValue()).isEqualTo(expectedNotificationMessage); + softly.assertThat(actual.getNotificationType()).isEqualTo(expectedNotificationType); + softly.assertThat(actual.getNotificationReferencedId().getValue()).isEqualTo(expectedReferencedId); + softly.assertThat(actual.getIsRead().getValue()).isEqualTo(expectedIsRead); + softly.assertThat(actual.getMember()).isEqualTo(expectedMember); + }); + } + + @DisplayName("러너 게시글에 서포터를 할당했다는 알림을 생성한다.") + @Test + void subscribeRunnerPostAssignSupporterEvent() { + // given + final Runner runner = persistRunner(MemberFixture.createHyena()); + final RunnerPost runnerPost = persistRunnerPost(runner); + final Supporter targetSupporter = persistSupporter(MemberFixture.createEthan()); + + persistApplicant(targetSupporter, runnerPost); + persistAssignSupporter(targetSupporter, runnerPost); + + // when + final RunnerPostAssignSupporterEvent event = new RunnerPostAssignSupporterEvent(runnerPost.getId()); + notificationEventListener.subscribeRunnerPostAssignSupporterEvent(event); + + // then + final List actualNotifications = notificationQuerydslRepository.findByMemberId(targetSupporter.getMember().getId(), 10); + + final String expectedNotificationTitle = "코드 리뷰 매칭이 완료되었습니다."; + final String expectedNotificationMessage = String.format("관련 게시글 - %s", runnerPost.getTitle().getValue()); + final NotificationType expectedNotificationType = NotificationType.RUNNER_POST; + final Long expectedReferencedId = runnerPost.getId(); + final boolean expectedIsRead = false; + final Member expectedMember = targetSupporter.getMember(); + + assertSoftly(softly -> { + softly.assertThat(actualNotifications).hasSize(1); + final Notification actual = actualNotifications.get(0); + + softly.assertThat(actual.getId()).isPositive(); + softly.assertThat(actual.getNotificationTitle().getValue()).isEqualTo(expectedNotificationTitle); + softly.assertThat(actual.getNotificationMessage().getValue()).isEqualTo(expectedNotificationMessage); + softly.assertThat(actual.getNotificationType()).isEqualTo(expectedNotificationType); + softly.assertThat(actual.getNotificationReferencedId().getValue()).isEqualTo(expectedReferencedId); + softly.assertThat(actual.getIsRead().getValue()).isEqualTo(expectedIsRead); + softly.assertThat(actual.getMember()).isEqualTo(expectedMember); + }); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/notification/command/repository/NotificationCommandRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/notification/command/repository/NotificationCommandRepositoryTest.java new file mode 100644 index 000000000..219bb5c91 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/notification/command/repository/NotificationCommandRepositoryTest.java @@ -0,0 +1,39 @@ +package touch.baton.domain.notification.command.repository; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import touch.baton.config.RepositoryTestConfig; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.notification.command.Notification; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.fixture.domain.MemberFixture; + +import static org.assertj.core.api.Assertions.assertThat; +import static touch.baton.fixture.vo.NotificationReferencedIdFixture.notificationReferencedId; + +class NotificationCommandRepositoryTest extends RepositoryTestConfig { + + @Autowired + private NotificationCommandRepository notificationCommandRepository; + + @DisplayName("알림을 삭제할 경우 hard delete 가 아닌 soft delete 로 진행한다.") + @Test + void soft_delete() { + // given + final Runner runner = persistRunner(MemberFixture.createHyena()); + final RunnerPost runnerPost = persistRunnerPost(runner); + + final Notification notification = persistNotification(runner.getMember(), notificationReferencedId(runnerPost.getId())); + + // when + notificationCommandRepository.deleteById(notification.getId()); + + // then + final Notification foundNotification = (Notification) em.createNativeQuery("select * from notification where id = :id", Notification.class) + .setParameter("id", notification.getId()) + .getSingleResult(); + + assertThat(foundNotification.getDeletedAt()).isNotNull(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/notification/command/service/NotificationCommandServiceTest.java b/backend/baton/src/test/java/touch/baton/domain/notification/command/service/NotificationCommandServiceTest.java new file mode 100644 index 000000000..d4dc78fb3 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/notification/command/service/NotificationCommandServiceTest.java @@ -0,0 +1,99 @@ +package touch.baton.domain.notification.command.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.notification.command.Notification; +import touch.baton.domain.notification.command.vo.NotificationReferencedId; +import touch.baton.domain.notification.exception.NotificationBusinessException; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.notification.exception.NotificationDomainException; +import touch.baton.fixture.domain.NotificationFixture; +import touch.baton.fixture.domain.MemberFixture; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static touch.baton.fixture.vo.NotificationReferencedIdFixture.notificationReferencedId; + +class NotificationCommandServiceTest extends ServiceTestConfig { + + private NotificationCommandService notificationCommandService; + + @BeforeEach + void setUp() { + notificationCommandService = new NotificationCommandService(notificationCommandRepository); + } + + @DisplayName("알림 읽은 여부를 true 로 업데이트에 성공한다.") + @Test + void success_updateNotificationIsReadByMember() { + // given + final Member targetMember = memberCommandRepository.save(MemberFixture.createHyena()); + final NotificationReferencedId notificationReferencedId = notificationReferencedId(1L); + final Notification savedNotification = notificationCommandRepository.save(NotificationFixture.create(targetMember, notificationReferencedId)); + + // when + notificationCommandService.updateNotificationIsReadTrueByMember(targetMember, savedNotification.getId()); + + // then + final Optional maybeActual = notificationCommandRepository.findById(savedNotification.getId()); + + assertSoftly(softly -> { + softly.assertThat(maybeActual).isPresent(); + final Notification actual = maybeActual.get(); + + softly.assertThat(actual.getIsRead().getValue()).isEqualTo(true); + }); + } + + @DisplayName("알림 읽은 여부를 읽음 상태로 업데이트 할 때 알림의 주인(사용자)가 아닐 경우 예외가 발생한다.") + @Test + void fail_updateNotificationIsReadByMember_when_member_isNotOwner() { + // given + final Member targetMember = memberCommandRepository.save(MemberFixture.createHyena()); + final NotificationReferencedId notificationReferencedId = notificationReferencedId(1L); + final Notification savedNotification = notificationCommandRepository.save(NotificationFixture.create(targetMember, notificationReferencedId)); + + final Member notOwner = memberCommandRepository.save(MemberFixture.createEthan()); + + // when & then + assertThatThrownBy(() -> notificationCommandService.updateNotificationIsReadTrueByMember(notOwner, savedNotification.getId())) + .isInstanceOf(NotificationDomainException.class); + } + + @DisplayName("알림 삭제를 성공한다.") + @Test + void success_deleteNotificationByMember() { + // given + final Member targetMember = memberCommandRepository.save(MemberFixture.createHyena()); + final NotificationReferencedId notificationReferencedId = notificationReferencedId(1L); + final Notification savedNotification = notificationCommandRepository.save(NotificationFixture.create(targetMember, notificationReferencedId)); + + // when + notificationCommandService.deleteNotificationByMember(targetMember, savedNotification.getId()); + + // then + final Optional actual = notificationCommandRepository.findById(savedNotification.getId()); + + assertThat(actual).isEmpty(); + } + + @DisplayName("알림 삭제을 삭제할 때 알림의 주인(사용자)가 아닐 경우 예외가 발생한다.") + @Test + void fail_deleteNotificationByMember_when_member_isNotOwner() { + // given + final Member targetMember = memberCommandRepository.save(MemberFixture.createHyena()); + final NotificationReferencedId notificationReferencedId = notificationReferencedId(1L); + final Notification savedNotification = notificationCommandRepository.save(NotificationFixture.create(targetMember, notificationReferencedId)); + + final Member notOwner = memberCommandRepository.save(MemberFixture.createEthan()); + + // when & then + assertThatThrownBy(() -> notificationCommandService.deleteNotificationByMember(notOwner, savedNotification.getId())) + .isInstanceOf(NotificationBusinessException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/notification/command/vo/IsReadTest.java b/backend/baton/src/test/java/touch/baton/domain/notification/command/vo/IsReadTest.java new file mode 100644 index 000000000..57b2d2f19 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/notification/command/vo/IsReadTest.java @@ -0,0 +1,44 @@ +package touch.baton.domain.notification.command.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.assertThatCode; + +class IsReadTest { + + @DisplayName("정적 팩터리 메서드로 읽음 여부 false 생성에 성공한다") + @Test + void success_create_isRead_false() { + assertThatCode(() -> IsRead.asUnRead()) + .doesNotThrowAnyException(); + } + + @DisplayName("정적 팩터리 메서드로 읽음 여부 false 를 생성 할 수 있다.") + @Test + void success_isRead_false() { + // given + final IsRead actual = IsRead.asUnRead(); + + // when & then + assertThat(actual.getValue()).isFalse(); + } + + @DisplayName("정적 팩터리 메서드로 읽음 여부 true 생성에 성공한다") + @Test + void success_create_isRead_true() { + assertThatCode(() -> IsRead.asRead()) + .doesNotThrowAnyException(); + } + + @DisplayName("정적 팩터리 메서드로 읽음 여부 true 를 생성 할 수 있다.") + @Test + void success_isRead_true() { + // given + final IsRead actual = IsRead.asRead(); + + // when & then + assertThat(actual.getValue()).isTrue(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/notification/command/vo/NotificationMessageTest.java b/backend/baton/src/test/java/touch/baton/domain/notification/command/vo/NotificationMessageTest.java new file mode 100644 index 000000000..4496c6989 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/notification/command/vo/NotificationMessageTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.notification.command.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class NotificationMessageTest { + + @DisplayName("value 가 null 이면 예외가 발생한다.") + @Test + void fail_if_value_is_null() { + assertThatThrownBy(() -> new NotificationMessage(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/notification/command/vo/NotificationReferencedIdTest.java b/backend/baton/src/test/java/touch/baton/domain/notification/command/vo/NotificationReferencedIdTest.java new file mode 100644 index 000000000..b79f2cf2a --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/notification/command/vo/NotificationReferencedIdTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.notification.command.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class NotificationReferencedIdTest { + + @DisplayName("value 가 null 이면 예외가 발생한다.") + @Test + void fail_if_value_is_null() { + assertThatThrownBy(() -> new NotificationReferencedId(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/notification/command/vo/NotificationTitleTest.java b/backend/baton/src/test/java/touch/baton/domain/notification/command/vo/NotificationTitleTest.java new file mode 100644 index 000000000..2a175f9b6 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/notification/command/vo/NotificationTitleTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.notification.command.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class NotificationTitleTest { + + @DisplayName("value 가 null 이면 예외가 발생한다.") + @Test + void fail_if_value_is_null() { + assertThatThrownBy(() -> new NotificationTitle(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/notification/query/repository/NotificationQuerydslRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/notification/query/repository/NotificationQuerydslRepositoryTest.java new file mode 100644 index 000000000..22d09f7c5 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/notification/query/repository/NotificationQuerydslRepositoryTest.java @@ -0,0 +1,71 @@ +package touch.baton.domain.notification.query.repository; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import touch.baton.config.RepositoryTestConfig; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.notification.command.Notification; +import touch.baton.fixture.domain.MemberFixture; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static touch.baton.fixture.vo.NotificationReferencedIdFixture.notificationReferencedId; + +class NotificationQuerydslRepositoryTest extends RepositoryTestConfig { + + @Autowired + private NotificationQuerydslRepository notificationQuerydslRepository; + + @DisplayName("deleted_at 이 null 인 경우의 알림을 조회하기 위해서는 nativeQuery 를 사용한다.") + @Test + void success_findAll_deletedAt_isNull() { + // given + final Member targetMember = persistRunner(MemberFixture.createHyena()).getMember(); + + final Notification deletedAtIsNotNullNotification = persistNotification(targetMember, notificationReferencedId(1L)); + final Notification deletedAtIsNullNotification = persistNotification(targetMember, notificationReferencedId(2L)); + + // when + em.remove(deletedAtIsNullNotification); + final List actual = em.createNativeQuery("select * from notification", Notification.class).getResultList(); + + + // then + assertThat(actual).containsExactly(deletedAtIsNotNullNotification, deletedAtIsNullNotification); + } + + @DisplayName("deleted_at 이 null 이 아닌 경우 알림 목록을 사용자 식별자값을 이용해서 알림 식별자값 기준으로 내림차순 정렬하여 알림 목록을 조회한다") + @Test + void findByMemberIdOrderByIdDescLimit() { + // given + final Member targetMember = persistRunner(MemberFixture.createHyena()).getMember(); + + final List savedNotifications = new ArrayList<>(); + for (long referencedId = 1; referencedId <= 20; referencedId++) { + final Notification savedNotification = persistNotification(targetMember, notificationReferencedId(referencedId)); + savedNotifications.add(savedNotification); + } + savedNotifications.sort(orderByNotificationIdDesc()); + + // when + final int limit = 10; + final List actual = notificationQuerydslRepository.findByMemberId(targetMember.getId(), limit); + + // then + final List expected = savedNotifications.subList(0, limit); + + assertSoftly(softly -> { + softly.assertThat(actual).isSortedAccordingTo(orderByNotificationIdDesc()); + softly.assertThat(actual).containsExactlyElementsOf(expected); + }); + } + + private Comparator orderByNotificationIdDesc() { + return (left, right) -> left.getId() < right.getId() ? 1 : -1; + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/notification/query/service/NotificationQueryServiceTest.java b/backend/baton/src/test/java/touch/baton/domain/notification/query/service/NotificationQueryServiceTest.java new file mode 100644 index 000000000..1a0bbf726 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/notification/query/service/NotificationQueryServiceTest.java @@ -0,0 +1,77 @@ +package touch.baton.domain.notification.query.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.notification.command.Notification; +import touch.baton.domain.member.command.Member; +import touch.baton.fixture.domain.MemberFixture; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static touch.baton.fixture.vo.NotificationReferencedIdFixture.notificationReferencedId; + +class NotificationQueryServiceTest extends ServiceTestConfig { + + private NotificationQueryService notificationQueryService; + + @BeforeEach + void setUp() { + notificationQueryService = new NotificationQueryService(notificationQuerydslRepository); + } + + @DisplayName("사용자 식별자값으로 알림 목록을 조회에 성공한다.") + @Test + void success_readNotificationsByMemberId() { + // given + final Member targetMember = memberCommandRepository.save(MemberFixture.createHyena()); + + + final List savedNotifications = new ArrayList<>(); + for (long referencedId = 1; referencedId <= 20; referencedId++) { + final Notification savedNotification = persistNotification(targetMember, notificationReferencedId(referencedId)); + savedNotifications.add(savedNotification); + } + savedNotifications.sort(orderByNotificationIdDesc()); + + // when + final int limit = 10; + final List actual = notificationQueryService.readNotificationsByMemberId(targetMember.getId(), limit); + + // then + final List expected = savedNotifications.subList(0, limit); + + assertSoftly(softly -> { + softly.assertThat(actual).isSortedAccordingTo(orderByNotificationIdDesc()); + softly.assertThat(actual).containsExactlyElementsOf(expected); + }); + } + + private Comparator orderByNotificationIdDesc() { + return (left, right) -> left.getId() < right.getId() ? 1 : -1; + } + + @DisplayName("사용자 식별자값으로 조회한 알림 목록이 비어있을 경우 빈 목록 반환한다.") + @Test + void success_readNotificationsByMemberId_when_Notifications_isEmpty() { + // given + final Member targetMember = memberCommandRepository.save(MemberFixture.createHyena()); + + // when + final int limit = 10; + final List actual = notificationQueryService.readNotificationsByMemberId(targetMember.getId(), limit); + + // then + final List expected = Collections.emptyList(); + + assertSoftly(softly -> { + softly.assertThat(actual).isSortedAccordingTo(orderByNotificationIdDesc()); + softly.assertThat(actual).containsExactlyElementsOf(expected); + }); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/oauth/command/controller/OauthTypeConverterTest.java b/backend/baton/src/test/java/touch/baton/domain/oauth/command/controller/OauthTypeConverterTest.java new file mode 100644 index 000000000..ea52ea4c1 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/oauth/command/controller/OauthTypeConverterTest.java @@ -0,0 +1,26 @@ +package touch.baton.domain.oauth.command.controller; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import touch.baton.config.converter.OauthTypeConverter; +import touch.baton.domain.oauth.command.OauthType; + +import static org.assertj.core.api.Assertions.assertThat; + +class OauthTypeConverterTest { + + @DisplayName("OauthType 이 github 으로 입력될 때 변환에 성공한다.") + @ParameterizedTest + @ValueSource(strings = {"github", "Github", "GitHub", "GITHUB"}) + void github(final String oauthTypeValue) { + // given + final OauthTypeConverter oauthTypeConverter = new OauthTypeConverter(); + + // when + final OauthType convertedOauthType = oauthTypeConverter.convert(oauthTypeValue); + + // then + assertThat(convertedOauthType).isEqualTo(OauthType.GITHUB); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/oauth/command/repository/RefreshTokenCommandRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/oauth/command/repository/RefreshTokenCommandRepositoryTest.java new file mode 100644 index 000000000..b66d7e744 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/oauth/command/repository/RefreshTokenCommandRepositoryTest.java @@ -0,0 +1,110 @@ +package touch.baton.domain.oauth.command.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.command.Member; +import touch.baton.domain.oauth.command.token.RefreshToken; +import touch.baton.domain.oauth.command.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.Assertions.assertThat; +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 RefreshTokenCommandRepositoryTest extends RepositoryTestConfig { + + @Autowired + private RefreshTokenCommandRepository refreshTokenCommandRepository; + + @Autowired + private EntityManager em; + + @DisplayName("리프레시 토큰을 토큰으로 찾을 수 있다.") + @Test + void findByToken() { + // given + final Member ethan = persistMember(MemberFixture.createEthan()); + final Member ditoo = persistMember(MemberFixture.createDitoo()); + + 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 = refreshTokenCommandRepository.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 = persistMember(MemberFixture.createEthan()); + final Member notOwner = persistMember(MemberFixture.createDitoo()); + + 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 = refreshTokenCommandRepository.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()); + }); + } + + @DisplayName("사용자를 이용해 리프레시 토큰을 삭제할 수 있다.") + @Test + void logout() { + // given + final Member owner = persistMember(MemberFixture.createEthan()); + + final LocalDateTime expireDate = createExpireDate(now().plusDays(30)); + + final RefreshToken expected = RefreshTokenFixture.create(owner, token("ethan RefreshToken"), expireDate(expireDate)); + em.persist(expected); + + em.flush(); + em.clear(); + + // when + refreshTokenCommandRepository.deleteByMember(owner); + + // then + assertThat(refreshTokenCommandRepository.findByMember(owner)).isNotPresent(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/oauth/command/service/OauthCommandServiceDeleteTest.java b/backend/baton/src/test/java/touch/baton/domain/oauth/command/service/OauthCommandServiceDeleteTest.java new file mode 100644 index 000000000..7d5ebf654 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/oauth/command/service/OauthCommandServiceDeleteTest.java @@ -0,0 +1,72 @@ +package touch.baton.domain.oauth.command.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.command.Member; +import touch.baton.domain.oauth.command.authcode.AuthCodeRequestUrlProviderComposite; +import touch.baton.domain.oauth.command.client.OauthInformationClientComposite; +import touch.baton.domain.oauth.command.repository.OauthMemberCommandRepository; +import touch.baton.domain.oauth.command.repository.OauthRunnerCommandRepository; +import touch.baton.domain.oauth.command.repository.OauthSupporterCommandRepository; +import touch.baton.domain.oauth.command.repository.RefreshTokenCommandRepository; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.infra.auth.jwt.JwtDecoder; +import touch.baton.infra.auth.jwt.JwtEncoder; + +import static org.mockito.BDDMockito.verify; +import static org.mockito.Mockito.only; + +@ExtendWith(MockitoExtension.class) +class OauthCommandServiceDeleteTest { + + private OauthCommandService oauthCommandService; + + @Mock + private AuthCodeRequestUrlProviderComposite authCodeRequestUrlProviderComposite; + + @Mock + private OauthInformationClientComposite oauthInformationClientComposite; + + @Mock + private OauthMemberCommandRepository oauthMemberCommandRepository; + + @Mock + private OauthRunnerCommandRepository oauthRunnerCommandRepository; + + @Mock + private OauthSupporterCommandRepository oauthSupporterCommandRepository; + + @Mock + private RefreshTokenCommandRepository refreshTokenCommandRepository; + + @Mock + private JwtEncoder jwtEncoder; + + @Mock + private JwtEncoder expiredJwtEncoder; + + @Mock + private JwtDecoder jwtDecoder; + + @BeforeEach + void setUp() { + oauthCommandService = new OauthCommandService(authCodeRequestUrlProviderComposite, oauthInformationClientComposite, oauthMemberCommandRepository, oauthRunnerCommandRepository, oauthSupporterCommandRepository, refreshTokenCommandRepository, jwtEncoder, jwtDecoder); + } + + @DisplayName("Member 로 RefreshToken 을 삭제할 수 있다.") + @Test + void success_logout() { + // given + final Member ethan = MemberFixture.createEthan(); + + // when + oauthCommandService.logout(ethan); + + // then + verify(refreshTokenCommandRepository, only()).deleteByMember(ethan); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/oauth/command/service/OauthCommandServiceUpdateTest.java b/backend/baton/src/test/java/touch/baton/domain/oauth/command/service/OauthCommandServiceUpdateTest.java new file mode 100644 index 000000000..cdcb99b34 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/oauth/command/service/OauthCommandServiceUpdateTest.java @@ -0,0 +1,193 @@ +package touch.baton.domain.oauth.command.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.command.Member; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.oauth.command.AuthorizationHeader; +import touch.baton.domain.oauth.command.authcode.AuthCodeRequestUrlProviderComposite; +import touch.baton.domain.oauth.command.client.OauthInformationClientComposite; +import touch.baton.domain.oauth.command.exception.OauthRequestException; +import touch.baton.domain.oauth.command.repository.OauthMemberCommandRepository; +import touch.baton.domain.oauth.command.repository.OauthRunnerCommandRepository; +import touch.baton.domain.oauth.command.repository.OauthSupporterCommandRepository; +import touch.baton.domain.oauth.command.repository.RefreshTokenCommandRepository; +import touch.baton.domain.oauth.command.token.ExpireDate; +import touch.baton.domain.oauth.command.token.RefreshToken; +import touch.baton.domain.oauth.command.token.Token; +import touch.baton.domain.oauth.command.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 OauthCommandServiceUpdateTest { + + private OauthCommandService oauthCommandService; + + @Mock + private AuthCodeRequestUrlProviderComposite authCodeRequestUrlProviderComposite; + + @Mock + private OauthInformationClientComposite oauthInformationClientComposite; + + @Mock + private OauthMemberCommandRepository oauthMemberCommandRepository; + + @Mock + private OauthRunnerCommandRepository oauthRunnerCommandRepository; + + @Mock + private OauthSupporterCommandRepository oauthSupporterCommandRepository; + + @Mock + private RefreshTokenCommandRepository refreshTokenCommandRepository; + + 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); + + oauthCommandService = new OauthCommandService(authCodeRequestUrlProviderComposite, oauthInformationClientComposite, oauthMemberCommandRepository, oauthRunnerCommandRepository, oauthSupporterCommandRepository, refreshTokenCommandRepository, 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(oauthMemberCommandRepository.findBySocialId(eq(new SocialId(tokenOwner.getSocialId().getValue())))).willReturn(Optional.of(tokenOwner)); + given(refreshTokenCommandRepository.findByToken(eq(new Token(refreshTokenValue)))).willReturn(Optional.of(beforeRefreshToken)); + + // when + final Tokens tokens = oauthCommandService.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(() -> oauthCommandService.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(oauthMemberCommandRepository.findBySocialId(eq(new SocialId(tokenOwner.getSocialId().getValue())))).willReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> oauthCommandService.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(oauthMemberCommandRepository.findBySocialId(eq(new SocialId(tokenOwner.getSocialId().getValue())))).willReturn(Optional.of(tokenOwner)); + given(refreshTokenCommandRepository.findByToken(eq(new Token(refreshTokenValue)))).willReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> oauthCommandService.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(oauthMemberCommandRepository.findBySocialId(eq(new SocialId(tokenOwner.getSocialId().getValue())))).willReturn(Optional.of(notTokenOwner)); + given(refreshTokenCommandRepository.findByToken(eq(new Token(refreshTokenValue)))).willReturn(Optional.of(beforeRefreshToken)); + + // when, then + assertThatThrownBy(() -> oauthCommandService.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(oauthMemberCommandRepository.findBySocialId(eq(new SocialId(tokenOwner.getSocialId().getValue())))).willReturn(Optional.of(tokenOwner)); + given(refreshTokenCommandRepository.findByToken(eq(new Token(refreshTokenValue)))).willReturn(Optional.of(beforeRefreshToken)); + + // when, then + assertThatThrownBy(() -> oauthCommandService.reissueAccessToken(expiredAuthorizationHeader, refreshTokenValue)).isInstanceOf(OauthRequestException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/oauth/command/token/RefreshTokenTest.java b/backend/baton/src/test/java/touch/baton/domain/oauth/command/token/RefreshTokenTest.java new file mode 100644 index 000000000..3ff96740e --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/oauth/command/token/RefreshTokenTest.java @@ -0,0 +1,157 @@ +package touch.baton.domain.oauth.command.token; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.vo.Company; +import touch.baton.domain.member.command.vo.GithubUrl; +import touch.baton.domain.member.command.vo.ImageUrl; +import touch.baton.domain.member.command.vo.MemberName; +import touch.baton.domain.member.command.vo.OauthId; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.oauth.command.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.*; +import static org.junit.jupiter.api.Assertions.assertAll; +import static touch.baton.domain.oauth.command.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 index 36271b0b8..7f465ca03 100644 --- 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 @@ -3,11 +3,9 @@ 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 touch.baton.domain.oauth.command.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.Assertions.*; import static org.assertj.core.api.SoftAssertions.assertSoftly; class AuthorizationHeaderTest { diff --git a/backend/baton/src/test/java/touch/baton/domain/runner/RunnerTest.java b/backend/baton/src/test/java/touch/baton/domain/runner/RunnerTest.java index 8143ab4e5..358f7f8fd 100644 --- a/backend/baton/src/test/java/touch/baton/domain/runner/RunnerTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/runner/RunnerTest.java @@ -3,15 +3,16 @@ 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.runner.exception.RunnerDomainException; -import touch.baton.domain.technicaltag.RunnerTechnicalTags; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.vo.Company; +import touch.baton.domain.member.command.vo.GithubUrl; +import touch.baton.domain.member.command.vo.ImageUrl; +import touch.baton.domain.member.command.vo.MemberName; +import touch.baton.domain.member.command.vo.OauthId; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.member.exception.RunnerDomainException; +import touch.baton.domain.technicaltag.command.RunnerTechnicalTags; import java.util.ArrayList; diff --git a/backend/baton/src/test/java/touch/baton/domain/runner/repository/RunnerQueryRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/runner/repository/RunnerQueryRepositoryTest.java new file mode 100644 index 000000000..a116519cd --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runner/repository/RunnerQueryRepositoryTest.java @@ -0,0 +1,92 @@ +package touch.baton.domain.runner.repository; + +import org.junit.jupiter.api.BeforeEach; +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.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.repository.MemberCommandRepository; +import touch.baton.domain.member.command.vo.Company; +import touch.baton.domain.member.command.vo.GithubUrl; +import touch.baton.domain.member.command.vo.ImageUrl; +import touch.baton.domain.member.command.vo.MemberName; +import touch.baton.domain.member.command.vo.OauthId; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.member.query.repository.RunnerQueryRepository; +import touch.baton.fixture.domain.RunnerTechnicalTagsFixture; + +import java.util.ArrayList; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class RunnerQueryRepositoryTest extends RepositoryTestConfig { + + private static final MemberName memberName = new MemberName("헤에디주"); + private static final SocialId socialId = new SocialId("testSocialId"); + private static final OauthId oauthId = new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j"); + private static final GithubUrl githubUrl = new GithubUrl("github.com/hyena0608"); + private static final Company company = new Company("우아한형제들"); + private static final ImageUrl imageUrl = new ImageUrl("김석호"); + + @Autowired + private RunnerQueryRepository runnerQueryRepository; + + @Autowired + private MemberCommandRepository memberCommandRepository; + + private Runner runner; + + @BeforeEach + void setUp() { + final Member member = Member.builder() + .memberName(memberName) + .socialId(socialId) + .oauthId(oauthId) + .githubUrl(githubUrl) + .company(company) + .imageUrl(imageUrl) + .build(); + memberCommandRepository.save(member); + + runner = Runner.builder() + .member(member) + .runnerTechnicalTags(RunnerTechnicalTagsFixture.create(new ArrayList<>())) + .build(); + } + + @DisplayName("Runner 를 Member 와 조인해서 조회할 수 있다.") + @Test + void findByIdJoinMember() { + // given + final Runner expected = runnerQueryRepository.save(runner); + + // when + final Optional actual = runnerQueryRepository.joinMemberByRunnerId(expected.getId()); + + // then + assertThat(actual).isPresent(); + final Member actualMember = actual.get().getMember(); + assertAll( + () -> assertThat(actualMember.getId()).isNotNull(), + () -> assertThat(actualMember.getMemberName()).isEqualTo(memberName), + () -> assertThat(actualMember.getCompany()).isEqualTo(company), + () -> assertThat(actualMember.getSocialId()).isEqualTo(socialId), + () -> assertThat(actualMember.getOauthId()).isEqualTo(oauthId), + () -> assertThat(actualMember.getGithubUrl()).isEqualTo(githubUrl) + ); + } + + @DisplayName("식별자가 없으면 Optional.empty()가 반환된다.") + @Test + void findByIdJoinMember_if_id_is_not_exists() { + // when + final Optional actual = runnerQueryRepository.joinMemberByRunnerId(999L); + + // then + assertThat(actual).isEmpty(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runner/service/RunnerCommandServiceTest.java b/backend/baton/src/test/java/touch/baton/domain/runner/service/RunnerCommandServiceTest.java new file mode 100644 index 000000000..02a120d5c --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runner/service/RunnerCommandServiceTest.java @@ -0,0 +1,69 @@ +package touch.baton.domain.runner.service; + +import org.jetbrains.annotations.NotNull; +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.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.service.RunnerCommandService; +import touch.baton.domain.member.command.service.dto.RunnerUpdateRequest; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.RunnerFixture; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +class RunnerCommandServiceTest extends ServiceTestConfig { + + private RunnerCommandService runnerCommandService; + + @BeforeEach + void setUp() { + runnerCommandService = new RunnerCommandService(runnerTechnicalTagCommandRepository, technicalTagQueryRepository); + } + + @DisplayName("Runner 의 프로필을 수정한다.") + @Test + void updateRunnerProfile() { + // given + final Member memberJudy = memberCommandRepository.save(MemberFixture.createJudy()); + final Runner runnerJudy = runnerQueryRepository.save(RunnerFixture.createRunner(memberJudy)); + final RunnerUpdateRequest runnerUpdateRequest = new RunnerUpdateRequest("변경된 이름", "변경된 회사", "변경된 자기소개", List.of("changedTag1", "changedTag2")); + + // when, then + assertThatCode(() -> runnerCommandService.updateRunner(runnerJudy, runnerUpdateRequest)) + .doesNotThrowAnyException(); + } + + @DisplayName("수정된 Runner 의 프로필을 조회하고 검증한다.") + @Test + void readUpdatedRunnerProfile() { + // given + final Member memberJudy = memberCommandRepository.save(MemberFixture.createJudy()); + final Runner runnerJudy = runnerQueryRepository.save(RunnerFixture.createRunner(memberJudy)); + final RunnerUpdateRequest runnerUpdateRequest = new RunnerUpdateRequest("변경된 이름", "변경된 회사", "변경된 자기소개", List.of("changedTag1", "changedTag2")); + + runnerCommandService.updateRunner(runnerJudy, runnerUpdateRequest); + final Runner foundRunnerJudy = runnerQueryRepository.findById(runnerJudy.getId()).get(); + + // when, then + assertAll( + () -> assertThat(foundRunnerJudy.getMember().getMemberName().getValue()).isEqualTo(runnerUpdateRequest.name()), + () -> assertThat(foundRunnerJudy.getMember().getCompany().getValue()).isEqualTo(runnerUpdateRequest.company()), + () -> assertThat(foundRunnerJudy.getIntroduction().getValue()).isEqualTo(runnerUpdateRequest.introduction()), + () -> assertThat(getRunnerTechnicalTags(foundRunnerJudy)).isEqualTo(runnerUpdateRequest.technicalTags()) + ); + } + + @NotNull + private List getRunnerTechnicalTags(final Runner foundRunnerJudy) { + return foundRunnerJudy.getRunnerTechnicalTags().getRunnerTechnicalTags().stream() + .map(runnerTechnicalTag -> runnerTechnicalTag.getTechnicalTag().getTagName().getValue()) + .toList(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runner/service/RunnerQueryServiceTest.java b/backend/baton/src/test/java/touch/baton/domain/runner/service/RunnerQueryServiceTest.java new file mode 100644 index 000000000..1afe441dd --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runner/service/RunnerQueryServiceTest.java @@ -0,0 +1,58 @@ +package touch.baton.domain.runner.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.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.query.service.RunnerQueryService; +import touch.baton.domain.technicaltag.command.TechnicalTag; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.RunnerFixture; +import touch.baton.fixture.domain.TechnicalTagFixture; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class RunnerQueryServiceTest extends ServiceTestConfig { + + private RunnerQueryService runnerQueryService; + + @BeforeEach + void setUp() { + runnerQueryService = new RunnerQueryService(runnerQueryRepository); + } + + @DisplayName("러너를 사용자와 함께 조회한다.") + @Test + void readRunnerWithMember() { + // given + final Member expectedMember = memberCommandRepository.save(MemberFixture.createEthan()); + + final TechnicalTag javaTag = technicalTagQueryRepository.save(TechnicalTagFixture.createJava()); + final TechnicalTag reactTag = technicalTagQueryRepository.save(TechnicalTagFixture.createReact()); + final List technicalTags = List.of(javaTag, reactTag); + final Runner expectedRunner = runnerQueryRepository.save(RunnerFixture.createRunner(expectedMember, technicalTags)); + + // when + final Runner actualRunner = runnerQueryService.readByRunnerId(expectedRunner.getId()); + + // then + final Member actualMember = actualRunner.getMember(); + assertAll( + () -> assertThat(actualRunner.getId()).isEqualTo(expectedRunner.getId()), + () -> assertThat(actualRunner.getIntroduction()).isEqualTo(expectedRunner.getIntroduction()), + () -> assertThat(actualRunner.getRunnerTechnicalTags().getRunnerTechnicalTags()).containsExactlyElementsOf(expectedRunner.getRunnerTechnicalTags().getRunnerTechnicalTags()), + () -> assertThat(actualMember.getId()).isEqualTo(expectedMember.getId()), + () -> assertThat(actualMember.getMemberName()).isEqualTo(expectedMember.getMemberName()), + () -> assertThat(actualMember.getSocialId()).isEqualTo(expectedMember.getSocialId()), + () -> assertThat(actualMember.getOauthId()).isEqualTo(expectedMember.getOauthId()), + () -> assertThat(actualMember.getGithubUrl()).isEqualTo(expectedMember.getGithubUrl()), + () -> assertThat(actualMember.getCompany()).isEqualTo(expectedMember.getCompany()), + () -> assertThat(actualMember.getImageUrl()).isEqualTo(expectedMember.getImageUrl()) + ); + } +} 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 b49968c4b..f92dbdb91 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 @@ -6,30 +6,31 @@ 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.Title; -import touch.baton.domain.common.vo.WatchedCount; -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.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; -import touch.baton.domain.supporter.vo.ReviewCount; -import touch.baton.domain.tag.RunnerPostTag; -import touch.baton.domain.tag.RunnerPostTags; -import touch.baton.domain.tag.Tag; -import touch.baton.domain.technicaltag.SupporterTechnicalTags; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.vo.Company; +import touch.baton.domain.member.command.vo.GithubUrl; +import touch.baton.domain.member.command.vo.ImageUrl; +import touch.baton.domain.member.command.vo.MemberName; +import touch.baton.domain.member.command.vo.OauthId; +import touch.baton.domain.member.command.vo.ReviewCount; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.exception.RunnerPostDomainException; +import touch.baton.domain.runnerpost.command.vo.CuriousContents; +import touch.baton.domain.runnerpost.command.vo.Deadline; +import touch.baton.domain.runnerpost.command.vo.ImplementedContents; +import touch.baton.domain.runnerpost.command.vo.IsReviewed; +import touch.baton.domain.runnerpost.command.vo.PostscriptContents; +import touch.baton.domain.runnerpost.command.vo.PullRequestUrl; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.command.vo.Title; +import touch.baton.domain.runnerpost.command.vo.WatchedCount; +import touch.baton.domain.tag.command.RunnerPostTag; +import touch.baton.domain.tag.command.RunnerPostTags; +import touch.baton.domain.tag.command.Tag; +import touch.baton.domain.technicaltag.command.SupporterTechnicalTags; import touch.baton.fixture.domain.RunnerTechnicalTagsFixture; import java.time.LocalDateTime; @@ -39,7 +40,9 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.assertj.core.api.Assertions.*; +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; import static org.junit.jupiter.api.Assertions.assertAll; @@ -98,7 +101,7 @@ void addAllRunnerPostTags() { // when runnerPost.addAllRunnerPostTags(List.of(java, spring)); - List runnerPostTags = runnerPost.getRunnerPostTags().getRunnerPostTags(); + final List runnerPostTags = runnerPost.getRunnerPostTags().getRunnerPostTags(); final List actualTagNames = runnerPostTags.stream() .map(runnerPostTag -> runnerPostTag.getTag().getTagName().getValue()) .collect(Collectors.toList()); @@ -479,7 +482,7 @@ void fail_NOT_STARTED__to_IN_PROGRESS() { .runnerPostTags(new RunnerPostTags(new ArrayList<>())) .build(); - // when & then + // when, then assertThatThrownBy(() -> runnerPost.updateReviewStatus(ReviewStatus.IN_PROGRESS)) .isInstanceOf(RunnerPostDomainException.class); } @@ -503,7 +506,7 @@ void fail_NOT_STARTED__to_DONE() { .runnerPostTags(new RunnerPostTags(new ArrayList<>())) .build(); - // when & then + // when, then assertThatThrownBy(() -> runnerPost.updateReviewStatus(ReviewStatus.DONE)) .isInstanceOf(RunnerPostDomainException.class); } @@ -527,7 +530,7 @@ void fail_DONE_to_NOT_STARTED() { .runnerPostTags(new RunnerPostTags(new ArrayList<>())) .build(); - // when & then + // when, then assertThatThrownBy(() -> runnerPost.updateReviewStatus(ReviewStatus.NOT_STARTED)) .isInstanceOf(RunnerPostDomainException.class); } @@ -551,7 +554,7 @@ void fail_DONE_to_IN_PROGRESS() { .runnerPostTags(new RunnerPostTags(new ArrayList<>())) .build(); - // when & then + // when, then assertThatThrownBy(() -> runnerPost.updateReviewStatus(ReviewStatus.IN_PROGRESS)) .isInstanceOf(RunnerPostDomainException.class); } @@ -576,7 +579,7 @@ void fail_same_to_same(final ReviewStatus reviewStatus) { .runnerPostTags(new RunnerPostTags(new ArrayList<>())) .build(); - // when & then + // when, then assertThatThrownBy(() -> runnerPost.updateReviewStatus(reviewStatus)) .isInstanceOf(RunnerPostDomainException.class); } diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/RunnerPostsApplicantCountTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/RunnerPostsApplicantCountTest.java new file mode 100644 index 000000000..3bb3a3242 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/RunnerPostsApplicantCountTest.java @@ -0,0 +1,74 @@ +package touch.baton.domain.runnerpost; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import touch.baton.domain.runnerpost.command.RunnerPostsApplicantCount; +import touch.baton.domain.runnerpost.command.exception.RunnerPostBusinessException; +import touch.baton.domain.runnerpost.command.repository.dto.RunnerPostApplicantCountDto; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +class RunnerPostsApplicantCountTest { + + @DisplayName("게시글 지원자 수 DTO 목록으로 생성한다.") + @Test + void createByRunnerPostApplicantCountDto() { + // given + final List dtos = new ArrayList<>(); + final long applicantCount = 3L; + for (long runnerPostId = 1; runnerPostId <= 10; runnerPostId++) { + dtos.add(new RunnerPostApplicantCountDto(runnerPostId, applicantCount)); + } + + // when, then + assertThatCode(() -> RunnerPostsApplicantCount.from(dtos)) + .doesNotThrowAnyException(); + } + + @DisplayName("게시글 지원자 수 DTO 목록이 null 이면 예외가 발생한다.") + @Test + void createByRunnerPostApplicantCountDto_fail_when_dto_is_null() { + // given + final List dtos = null; + + // when, then + assertThatThrownBy(() -> RunnerPostsApplicantCount.from(dtos)) + .isInstanceOf(RunnerPostBusinessException.class); + } + + @DisplayName("러너 게시글 지원자 수를 조회한다.") + @Test + void readRunnerPostsApplicantCount() { + // given + final List dtos = new ArrayList<>(); + final Long runnerPostId = 1L; + final Long applicantCount = 3L; + dtos.add(new RunnerPostApplicantCountDto(runnerPostId, applicantCount)); + final RunnerPostsApplicantCount runnerPostsApplicantCount = RunnerPostsApplicantCount.from(dtos); + + // when + final Long actual = runnerPostsApplicantCount.getApplicantCountById(runnerPostId); + + // then + assertThat(actual).isEqualTo(applicantCount); + } + + @DisplayName("러너 게시글 지원자 수를 조회할 때 없는 runnerPostId 로 조회하면 예외가 발생한다.") + @Test + void readRunnerPostsApplicantCount_fail_if_id_not_exist() { + // given + final List dtos = new ArrayList<>(); + final Long runnerPostId = 1L; + final long applicantCount = 3L; + dtos.add(new RunnerPostApplicantCountDto(runnerPostId, applicantCount)); + final RunnerPostsApplicantCount runnerPostsApplicantCount = RunnerPostsApplicantCount.from(dtos); + + // when, then + final Long invalidRunnerPostId = 0L; + assertThatThrownBy(() -> runnerPostsApplicantCount.getApplicantCountById(invalidRunnerPostId)) + .isInstanceOf(RunnerPostBusinessException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/exception/validator/UrlValidatorTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/exception/validator/UrlValidatorTest.java new file mode 100644 index 000000000..83b550f6f --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/exception/validator/UrlValidatorTest.java @@ -0,0 +1,121 @@ +package touch.baton.domain.runnerpost.command.exception.validator; + +import jakarta.validation.ClockProvider; +import jakarta.validation.ConstraintValidatorContext; +import jakarta.validation.Payload; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.ClientRequestException; + +import java.lang.annotation.Annotation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class UrlValidatorTest { + + @DisplayName("URL을 검증한다.") + @Nested + class isValid { + + @DisplayName("옳은 URL이면 통과한다.") + @ValueSource(strings = {"https://www.naver.com", + "https://www.naver.com", + "https://github.com/cookienc", + "http://dev-king-ethan.n-e.kr", + "https://www.naver.com/hello", + "https://www.naver.com/hello?world=123", + "https://github.com/twitter/the-algorithm/pull/1740" + }) + @ParameterizedTest + void success(final String target) { + // given + final UrlValidator urlValidator = new UrlValidator(); + urlValidator.initialize(new MockValidNotUrl()); + final MockConstraintValidatorContext mockconstraintValidatorContext = new MockConstraintValidatorContext(); + + // when, then + assertThat(urlValidator.isValid(target, mockconstraintValidatorContext)).isTrue(); + } + + @DisplayName("옳지 않은 URL이면 통과하지 않는다.") + @ValueSource(strings = {"https;//github.com/hello", + "https://", + "http://", + "URL 아님", + "github.com/twitter/the-algorithm/pull/1740", + "htts:github.com/twitter/the-algorithm/pull/1740" + }) + @ParameterizedTest + void fail_if_not_url(final String target) { + // given + final UrlValidator urlValidator = new UrlValidator(); + urlValidator.initialize(new MockValidNotUrl()); + final MockConstraintValidatorContext mockConstraintValidatorContext = new MockConstraintValidatorContext(); + + // when, then + assertThatThrownBy(() -> urlValidator.isValid(target, mockConstraintValidatorContext)) + .isInstanceOf(ClientRequestException.class); + } + } + + private static class MockConstraintValidatorContext implements ConstraintValidatorContext { + + @Override + public void disableDefaultConstraintViolation() { + + } + + @Override + public String getDefaultConstraintMessageTemplate() { + return null; + } + + @Override + public ClockProvider getClockProvider() { + return null; + } + + @Override + public ConstraintViolationBuilder buildConstraintViolationWithTemplate(final String messageTemplate) { + return null; + } + + @Override + public T unwrap(final Class type) { + return null; + } + } + + private static class MockValidNotUrl implements ValidNotUrl { + + @Override + public String message() { + return null; + } + + @Override + public Class[] groups() { + return null; + } + + @Override + public Class[] payload() { + return null; + } + + @Override + public ClientErrorCode clientErrorCode() { + return ClientErrorCode.PULL_REQUEST_URL_IS_NOT_URL; + } + + @Override + public Class annotationType() { + return null; + } + } + +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/repository/RunnerPostRepositoryDeleteTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/repository/RunnerPostRepositoryDeleteTest.java new file mode 100644 index 000000000..14380fbfb --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/repository/RunnerPostRepositoryDeleteTest.java @@ -0,0 +1,92 @@ +package touch.baton.domain.runnerpost.command.repository; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import touch.baton.config.RepositoryTestConfig; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.repository.MemberCommandRepository; +import touch.baton.domain.member.command.vo.Company; +import touch.baton.domain.member.command.vo.GithubUrl; +import touch.baton.domain.member.command.vo.ImageUrl; +import touch.baton.domain.member.command.vo.MemberName; +import touch.baton.domain.member.command.vo.OauthId; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.member.query.repository.RunnerQueryRepository; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.CuriousContents; +import touch.baton.domain.runnerpost.command.vo.Deadline; +import touch.baton.domain.runnerpost.command.vo.ImplementedContents; +import touch.baton.domain.runnerpost.command.vo.IsReviewed; +import touch.baton.domain.runnerpost.command.vo.PostscriptContents; +import touch.baton.domain.runnerpost.command.vo.PullRequestUrl; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.command.vo.Title; +import touch.baton.domain.runnerpost.command.vo.WatchedCount; +import touch.baton.domain.runnerpost.query.repository.RunnerPostQueryRepository; +import touch.baton.domain.tag.command.RunnerPostTags; +import touch.baton.fixture.domain.RunnerTechnicalTagsFixture; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +class RunnerPostRepositoryDeleteTest extends RepositoryTestConfig { + + @Autowired + private MemberCommandRepository memberCommandRepository; + + @Autowired + private RunnerQueryRepository runnerQueryRepository; + + @Autowired + private RunnerPostQueryRepository runnerPostQueryRepository; + + @DisplayName("RunnerPost 식별자값으로 RunnerPost 을 삭제한다.") + @Test + void success_deleteByRunnerPostId() { + // given + final Member member = Member.builder() + .memberName(new MemberName("헤에디주")) + .socialId(new SocialId("testSocialId")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("홍혁준")) + .build(); + final Member saveMember = memberCommandRepository.saveAndFlush(member); + + final Runner runner = Runner.builder() + .member(saveMember) + .runnerTechnicalTags(RunnerTechnicalTagsFixture.create(new ArrayList<>())) + .build(); + final Runner saveRunner = runnerQueryRepository.saveAndFlush(runner); + + final RunnerPost runnerPost = RunnerPost.builder() + .title(new Title("제 코드 리뷰 좀 해주세요!!")) + .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(); + final Long saveRunnerPostId = runnerPostQueryRepository.saveAndFlush(runnerPost).getId(); + + // when + runnerPostQueryRepository.deleteById(saveRunnerPostId); + + final Optional maybeRunnerPost = runnerPostQueryRepository.findById(saveRunnerPostId); + + // then + assertThat(maybeRunnerPost).isNotPresent(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/service/RunnerPostCommandServiceCreateTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/service/RunnerPostCommandServiceCreateTest.java new file mode 100644 index 000000000..e069c2963 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/service/RunnerPostCommandServiceCreateTest.java @@ -0,0 +1,167 @@ +package touch.baton.domain.runnerpost.command.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.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.SupporterRunnerPost; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.exception.RunnerPostBusinessException; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostApplicantCreateRequest; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostCreateRequest; +import touch.baton.domain.runnerpost.command.vo.CuriousContents; +import touch.baton.domain.runnerpost.command.vo.Deadline; +import touch.baton.domain.runnerpost.command.vo.ImplementedContents; +import touch.baton.domain.runnerpost.command.vo.PostscriptContents; +import touch.baton.domain.runnerpost.command.vo.PullRequestUrl; +import touch.baton.domain.runnerpost.command.vo.Title; +import touch.baton.domain.runnerpost.command.vo.WatchedCount; +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 java.util.Optional; + +import static java.time.LocalDateTime.now; +import static org.assertj.core.api.Assertions.assertThat; +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.fixture.vo.DeadlineFixture.deadline; + +class RunnerPostCommandServiceCreateTest extends ServiceTestConfig { + + private static final String TITLE = "코드 리뷰 해주세요."; + private static final String TAG = "Java"; + 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 IMPLEMENTED_CONTENTS = "이것 구현했어요."; + private static final String CURIOUS_CONTENTS = "궁금해."; + private static final String POSTSCRIPT_CONTENTS = "싸게 부탁드려요."; + + private RunnerPostCommandService runnerPostCommandService; + + @BeforeEach + void setUp() { + runnerPostCommandService = new RunnerPostCommandService( + runnerPostCommandRepository, + tagCommandRepository, + supporterCommandRepository, + supporterRunnerPostCommandRepository, + publisher + ); + } + + @DisplayName("Runner post 저장에 성공한다.") + @Test + void success() { + // given + final RunnerPostCreateRequest request = new RunnerPostCreateRequest(TITLE, + List.of(TAG, OTHER_TAG), + PULL_REQUEST_URL, + DEADLINE, + IMPLEMENTED_CONTENTS, + CURIOUS_CONTENTS, + POSTSCRIPT_CONTENTS); + final Member ethanMember = memberCommandRepository.save(MemberFixture.createEthan()); + final Runner runner = runnerQueryRepository.save(RunnerFixture.createRunner(ethanMember)); + + // when + final Long savedId = runnerPostCommandService.createRunnerPost(runner, request); + + // then + assertThat(savedId).isNotNull(); + final Optional maybeActual = runnerPostQueryRepository.findById(savedId); + assertThat(maybeActual).isPresent(); + final RunnerPost actual = maybeActual.get(); + assertAll( + () -> assertThat(actual.getTitle()).isEqualTo(new Title(TITLE)), + () -> 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)), + () -> assertThat(actual.getRunner()).isEqualTo(runner) + ); + } + + @DisplayName("Supporter 가 RunnerPost 에 리뷰를 지원한다.") + @Test + void success_createRunnerPostApplicant() { + // given + final Member savedMemberDitoo = memberCommandRepository.save(MemberFixture.createDitoo()); + final Runner savedRunnerDitto = runnerQueryRepository.save(RunnerFixture.createRunner(savedMemberDitoo)); + + final Member savedMemberHyena = memberCommandRepository.save(MemberFixture.createHyena()); + final Supporter savedSupporterHyena = supporterQueryRepository.save(SupporterFixture.create(savedMemberHyena)); + + final RunnerPost savedRunnerPost = runnerPostQueryRepository.save(RunnerPostFixture.create(savedRunnerDitto, deadline(now().plusHours(100)))); + + // when + final RunnerPostApplicantCreateRequest request = new RunnerPostApplicantCreateRequest("안녕하세요. 서포터 헤나입니다."); + final Long savedRunnerPostApplicantId = runnerPostCommandService.createRunnerPostApplicant(savedSupporterHyena, request, savedRunnerPost.getId()); + + final Optional maybeRunnerPostApplicant = supporterRunnerPostQueryRepository.findById(savedRunnerPostApplicantId); + + // then + assertSoftly(softly -> { + softly.assertThat(maybeRunnerPostApplicant).isPresent(); + softly.assertThat(maybeRunnerPostApplicant.get().getId()) + .isNotNull() + .isEqualTo(maybeRunnerPostApplicant.get().getId()); + softly.assertThat(maybeRunnerPostApplicant.get().getSupporter()).isEqualTo(savedSupporterHyena); + softly.assertThat(maybeRunnerPostApplicant.get().getRunnerPost()).isEqualTo(savedRunnerPost); + softly.assertThat(maybeRunnerPostApplicant.get().getMessage().getValue()).isEqualTo(request.message()); + }); + } + + @DisplayName("Supporter 가 RunnerPost 에 리뷰를 지원할 때 RunnerPost 가 존재하지 않을 경우 예외가 발생한다.") + @Test + void fail_createRunnerPostApplicant_if_runnerPost_is_null() { + // given + final Member savedMemberDitoo = memberCommandRepository.save(MemberFixture.createDitoo()); + final Runner savedRunnerDitto = runnerQueryRepository.save(RunnerFixture.createRunner(savedMemberDitoo)); + + final Member savedMemberHyena = memberCommandRepository.save(MemberFixture.createHyena()); + final Supporter savedSupporterHyena = supporterQueryRepository.save(SupporterFixture.create(savedMemberHyena)); + + // when + final RunnerPostApplicantCreateRequest request = new RunnerPostApplicantCreateRequest("안녕하세요. 서포터 헤나입니다."); + + // then + assertThatThrownBy(() -> runnerPostCommandService.createRunnerPostApplicant(savedSupporterHyena, request, 0L)) + .isInstanceOf(RunnerPostBusinessException.class); + } + + @DisplayName("Supporter 가 RunnerPost 에 리뷰를 지원할 때 이미 지원한 이력이 있는 경우 예외가 발생한다.") + @Test + void fail_createRunnerPostApplicant_if_supporter_already_applied() { + // given + final Member savedMemberDitoo = memberCommandRepository.save(MemberFixture.createDitoo()); + final Runner savedRunnerDitto = runnerQueryRepository.save(RunnerFixture.createRunner(savedMemberDitoo)); + + final Member savedMemberHyena = memberCommandRepository.save(MemberFixture.createHyena()); + final Supporter savedSupporterHyena = supporterQueryRepository.save(SupporterFixture.create(savedMemberHyena)); + + final RunnerPost savedRunnerPost = runnerPostQueryRepository.save(RunnerPostFixture.create(savedRunnerDitto, deadline(now().plusHours(100)))); + + // when + final RunnerPostApplicantCreateRequest request = new RunnerPostApplicantCreateRequest("안녕하세요. 서포터 헤나입니다."); + + // then + assertSoftly(softly -> { + softly.assertThatCode(() -> runnerPostCommandService.createRunnerPostApplicant(savedSupporterHyena, request, savedRunnerPost.getId())) + .doesNotThrowAnyException(); + softly.assertThatThrownBy(() -> runnerPostCommandService.createRunnerPostApplicant(savedSupporterHyena, request, savedRunnerPost.getId())) + .isInstanceOf(RunnerPostBusinessException.class); + }); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/service/RunnerPostCommandServiceDeleteTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/service/RunnerPostCommandServiceDeleteTest.java new file mode 100644 index 000000000..ea25d3e5b --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/service/RunnerPostCommandServiceDeleteTest.java @@ -0,0 +1,119 @@ +package touch.baton.domain.runnerpost.command.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.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.exception.RunnerPostBusinessException; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.RunnerFixture; +import touch.baton.fixture.domain.RunnerPostFixture; +import touch.baton.fixture.domain.SupporterFixture; +import touch.baton.fixture.domain.SupporterRunnerPostFixture; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static touch.baton.fixture.vo.DeadlineFixture.deadline; + +class RunnerPostCommandServiceDeleteTest extends ServiceTestConfig { + + private RunnerPostCommandService runnerPostCommandService; + + @BeforeEach + void setUp() { + runnerPostCommandService = new RunnerPostCommandService( + runnerPostCommandRepository, + tagCommandRepository, + supporterCommandRepository, + supporterRunnerPostCommandRepository, + publisher + ); + } + + @DisplayName("RunnerPost 식별자값으로 RunnerPost 을 삭제한다.") + @Test + void success_deleteByRunnerPostId() { + // given + final Member member = memberCommandRepository.save(MemberFixture.createDitoo()); + final Runner runner = runnerQueryRepository.save(RunnerFixture.createRunner(member)); + final RunnerPost runnerPost = runnerPostQueryRepository.save(RunnerPostFixture.create(runner, deadline(LocalDateTime.now().plusHours(10)))); + + // when + runnerPostCommandService.deleteByRunnerPostId(runnerPost.getId(), runner); + + // then + assertThat(runnerPostQueryRepository.existsById(runnerPost.getId())).isFalse(); + } + + @DisplayName("RunnerPost 식별자값으로 존재하지 않는 RunnerPost 를 삭제 시도할 경우 예외가 발생한다.") + @Test + void fail_deleteByRunnerPostId_if_runnerPost_is_null() { + final Member member = memberCommandRepository.save(MemberFixture.createDitoo()); + final Runner runner = runnerQueryRepository.save(RunnerFixture.createRunner(member)); + assertThatThrownBy(() -> runnerPostCommandService.deleteByRunnerPostId(0L, runner)) + .isInstanceOf(RunnerPostBusinessException.class); + } + + @DisplayName("RunnerPost 를 작성하지 않은 Runner 가 삭제 시도할 경우 예외가 발생한다.") + @Test + void fail_deleteByRunnerPostId_if_not_owner() { + // given + final Member memberRunnerPostOwner = memberCommandRepository.save(MemberFixture.createDitoo()); + final Runner runnerPostOwner = runnerQueryRepository.save(RunnerFixture.createRunner(memberRunnerPostOwner)); + final RunnerPost runnerPost = runnerPostQueryRepository.save(RunnerPostFixture.create( + runnerPostOwner, + deadline(LocalDateTime.now().plusHours(10)) + )); + final Member memberRunnerPostNotOwner = memberCommandRepository.save(MemberFixture.createJudy()); + final Runner runnerPostNotOwner = runnerQueryRepository.save(RunnerFixture.createRunner(memberRunnerPostNotOwner)); + + // when, then + assertThatThrownBy(() -> runnerPostCommandService.deleteByRunnerPostId(runnerPost.getId(), runnerPostNotOwner)) + .isInstanceOf(RunnerPostBusinessException.class); + } + + @DisplayName("NOT_STARTED 상태가 아닌 RunnerPost 를 삭제 시도할 경우 예외가 발생한다.") + @Test + void fail_deleteByRunnerPostId_if_reviewStatus_is_not_NOT_STARTED() { + // given + final Member memberRunner = memberCommandRepository.save(MemberFixture.createDitoo()); + final Runner runner = runnerQueryRepository.save(RunnerFixture.createRunner(memberRunner)); + final RunnerPost runnerPost = runnerPostQueryRepository.save(RunnerPostFixture.create( + runner, + deadline(LocalDateTime.now().plusHours(10)) + )); + final Member memberSupporter = memberCommandRepository.save(MemberFixture.createEthan()); + final Supporter supporter = supporterQueryRepository.save(SupporterFixture.create(memberSupporter)); + supporterRunnerPostQueryRepository.save(SupporterRunnerPostFixture.create(runnerPost, supporter)); + runnerPost.assignSupporter(supporter); + + // when, then + assertThatThrownBy(() -> runnerPostCommandService.deleteByRunnerPostId(runnerPost.getId(), runner)) + .isInstanceOf(RunnerPostBusinessException.class); + } + + @DisplayName("지원한 서포터가 있는 경우에 RunnerPost 를 삭제 시도할 경우 예외가 발생한다.") + @Test + void fail_deleteByRunnerPostId_if_applicant_is_exist() { + // given + final Member member = memberCommandRepository.save(MemberFixture.createDitoo()); + final Runner runner = runnerQueryRepository.save(RunnerFixture.createRunner(member)); + final RunnerPost runnerPost = runnerPostQueryRepository.save(RunnerPostFixture.create( + runner, + deadline(LocalDateTime.now().plusHours(10)) + )); + final Member memberSupporter = memberCommandRepository.save(MemberFixture.createEthan()); + final Supporter supporter = supporterQueryRepository.save(SupporterFixture.create(memberSupporter)); + supporterRunnerPostQueryRepository.save(SupporterRunnerPostFixture.create(runnerPost, supporter)); + + // when, then + assertThatThrownBy(() -> runnerPostCommandService.deleteByRunnerPostId(runnerPost.getId(), runner)) + .isInstanceOf(RunnerPostBusinessException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/service/RunnerPostCommandServiceEventTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/service/RunnerPostCommandServiceEventTest.java new file mode 100644 index 000000000..00042c60b --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/service/RunnerPostCommandServiceEventTest.java @@ -0,0 +1,119 @@ +package touch.baton.domain.runnerpost.command.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.event.ApplicationEvents; +import org.springframework.test.context.event.RecordApplicationEvents; +import touch.baton.config.ServiceTestConfig; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.event.RunnerPostApplySupporterEvent; +import touch.baton.domain.runnerpost.command.event.RunnerPostAssignSupporterEvent; +import touch.baton.domain.runnerpost.command.event.RunnerPostReviewStatusDoneEvent; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostApplicantCreateRequest; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostUpdateRequest; +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 static java.time.LocalDateTime.now; +import static org.assertj.core.api.Assertions.assertThat; +import static touch.baton.fixture.vo.DeadlineFixture.deadline; + +@RecordApplicationEvents +class RunnerPostCommandServiceEventTest extends ServiceTestConfig { + + @Autowired + private ApplicationEvents applicationEvents; + + private RunnerPostCommandService runnerPostCommandService; + + @BeforeEach + void setUp() { + runnerPostCommandService = new RunnerPostCommandService( + runnerPostCommandRepository, + tagCommandRepository, + supporterCommandRepository, + supporterRunnerPostCommandRepository, + publisher + ); + } + + @DisplayName("서포터가 러너 게시글에 리뷰를 지원하면 이벤트가 발행된다.") + @Test + void success_supporter_apply_runnerPost() { + // given + final Member savedMemberDitoo = memberCommandRepository.save(MemberFixture.createDitoo()); + final Runner savedRunnerDitto = runnerQueryRepository.save(RunnerFixture.createRunner(savedMemberDitoo)); + + final Member savedMemberHyena = memberCommandRepository.save(MemberFixture.createHyena()); + final Supporter savedSupporterHyena = supporterCommandRepository.save(SupporterFixture.create(savedMemberHyena)); + + final RunnerPost savedRunnerPost = runnerPostCommandRepository.save(RunnerPostFixture.create(savedRunnerDitto, deadline(now().plusHours(100)))); + + // when + final RunnerPostApplicantCreateRequest request = new RunnerPostApplicantCreateRequest("안녕하세요. 서포터 헤나입니다."); + runnerPostCommandService.createRunnerPostApplicant(savedSupporterHyena, request, savedRunnerPost.getId()); + + // then + final long eventPublishedCount = applicationEvents.stream(RunnerPostApplySupporterEvent.class).count(); + + assertThat(eventPublishedCount).isOne(); + } + + @DisplayName("러너는 자신의 러너 게시글의 지원자 중 한 명을 서포터로서 확정하면 이벤트가 발행된다.") + @Test + void success_runner_assign_applicant_supporter() { + // given + final Member savedMemberDitoo = memberCommandRepository.save(MemberFixture.createDitoo()); + final Runner savedRunnerDitto = runnerQueryRepository.save(RunnerFixture.createRunner(savedMemberDitoo)); + + final Member savedMemberHyena = memberCommandRepository.save(MemberFixture.createHyena()); + final Supporter savedSupporterHyena = supporterCommandRepository.save(SupporterFixture.create(savedMemberHyena)); + + final RunnerPost savedRunnerPost = runnerPostCommandRepository.save(RunnerPostFixture.create(savedRunnerDitto, deadline(now().plusHours(100)))); + + final RunnerPostApplicantCreateRequest runnerPostApplicantCreateRequest = new RunnerPostApplicantCreateRequest("안녕하세요. 서포터 헤나입니다."); + runnerPostCommandService.createRunnerPostApplicant(savedSupporterHyena, runnerPostApplicantCreateRequest, savedRunnerPost.getId()); + + // when + final RunnerPostUpdateRequest.SelectSupporter runnerPostAssignSupporterRequest = new RunnerPostUpdateRequest.SelectSupporter(savedSupporterHyena.getId()); + runnerPostCommandService.updateRunnerPostAppliedSupporter(savedRunnerDitto, savedRunnerPost.getId(), runnerPostAssignSupporterRequest); + + // then + final long eventPublishedCount = applicationEvents.stream(RunnerPostAssignSupporterEvent.class).count(); + + assertThat(eventPublishedCount).isOne(); + } + + @DisplayName("서포터가 러너 게시글의 상태를 리뷰 완료로 변경할 경우 이벤트가 발행된다.") + @Test + void success_supporter_update_runnerPost_reviewStatus_done() { + final Member savedMemberDitoo = memberCommandRepository.save(MemberFixture.createDitoo()); + final Runner savedRunnerDitto = runnerQueryRepository.save(RunnerFixture.createRunner(savedMemberDitoo)); + + final Member savedMemberHyena = memberCommandRepository.save(MemberFixture.createHyena()); + final Supporter savedSupporterHyena = supporterCommandRepository.save(SupporterFixture.create(savedMemberHyena)); + + final RunnerPost savedRunnerPost = runnerPostCommandRepository.save(RunnerPostFixture.create(savedRunnerDitto, deadline(now().plusHours(100)))); + + final RunnerPostApplicantCreateRequest runnerPostApplicantCreateRequest = new RunnerPostApplicantCreateRequest("안녕하세요. 서포터 헤나입니다."); + runnerPostCommandService.createRunnerPostApplicant(savedSupporterHyena, runnerPostApplicantCreateRequest, savedRunnerPost.getId()); + + final RunnerPostUpdateRequest.SelectSupporter runnerPostAssignSupporterRequest = new RunnerPostUpdateRequest.SelectSupporter(savedSupporterHyena.getId()); + runnerPostCommandService.updateRunnerPostAppliedSupporter(savedRunnerDitto, savedRunnerPost.getId(), runnerPostAssignSupporterRequest); + + // when + runnerPostCommandService.updateRunnerPostReviewStatusDone(savedRunnerPost.getId(), savedSupporterHyena); + + // then + final long eventPublishedCount = applicationEvents.stream(RunnerPostReviewStatusDoneEvent.class).count(); + + assertThat(eventPublishedCount).isOne(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/service/RunnerPostCommandServiceUpdateTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/service/RunnerPostCommandServiceUpdateTest.java new file mode 100644 index 000000000..4beaca789 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/service/RunnerPostCommandServiceUpdateTest.java @@ -0,0 +1,207 @@ +package touch.baton.domain.runnerpost.command.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.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.exception.RunnerPostBusinessException; +import touch.baton.domain.runnerpost.command.exception.RunnerPostDomainException; +import touch.baton.domain.runnerpost.command.service.dto.RunnerPostUpdateRequest; +import touch.baton.domain.runnerpost.command.vo.IsReviewed; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +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.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static touch.baton.domain.runnerpost.command.vo.ReviewStatus.IN_PROGRESS; +import static touch.baton.domain.runnerpost.command.vo.ReviewStatus.OVERDUE; +import static touch.baton.fixture.vo.DeadlineFixture.deadline; + +class RunnerPostCommandServiceUpdateTest extends ServiceTestConfig { + + private static Runner runnerPostOwner; + private static RunnerPost targetRunnerPost; + private static Supporter applySupporter; + private static Runner runner; + private static Supporter assignedSupporter; + + private RunnerPostCommandService runnerPostCommandService; + + @BeforeEach + void setUp() { + runnerPostCommandService = new RunnerPostCommandService( + runnerPostCommandRepository, + tagCommandRepository, + supporterCommandRepository, + supporterRunnerPostCommandRepository, + publisher + ); + + final Member ehtanMember = memberCommandRepository.save(MemberFixture.createEthan()); + runnerPostOwner = runnerQueryRepository.save(RunnerFixture.createRunner(ehtanMember)); + targetRunnerPost = runnerPostQueryRepository.save(RunnerPostFixture.create(runnerPostOwner, + deadline(LocalDateTime.now().plusDays(10)))); + + final Member hyenaMember = memberCommandRepository.save(MemberFixture.createHyena()); + applySupporter = supporterQueryRepository.save(SupporterFixture.create(hyenaMember)); + supporterRunnerPostQueryRepository.save(SupporterRunnerPostFixture.create(targetRunnerPost, applySupporter)); + + final Member runnerMember = memberCommandRepository.save(MemberFixture.createEthan()); + runner = runnerQueryRepository.save(RunnerFixture.createRunner(runnerMember)); + + final Member supporterMember = memberCommandRepository.save(MemberFixture.createDitoo()); + assignedSupporter = supporterQueryRepository.save(SupporterFixture.create(supporterMember)); + } + + @DisplayName("러너는 자신의 글에 제안한 서포터를 서포터로 선택할 수 있다.") + @Test + void updateRunnerPostAppliedSupporter() { + // given + final RunnerPostUpdateRequest.SelectSupporter request = new RunnerPostUpdateRequest.SelectSupporter(applySupporter.getId()); + + // when + runnerPostCommandService.updateRunnerPostAppliedSupporter(runnerPostOwner, targetRunnerPost.getId(), request); + + // then + final Optional maybeRunnerPost = runnerPostQueryRepository.findById(targetRunnerPost.getId()); + assertThat(maybeRunnerPost).isPresent(); + + final RunnerPost actualRunnerPost = maybeRunnerPost.get(); + assertAll( + () -> assertThat(actualRunnerPost.getSupporter().getId()).isEqualTo(applySupporter.getId()), + () -> assertThat(actualRunnerPost.getReviewStatus()).isEqualTo(ReviewStatus.IN_PROGRESS) + ); + } + + @DisplayName("러너는 가입되어 있지 않는 서포터를 선택할 수 없다.") + @Test + void fail_updateRunnerPostAppliedSupporter_if_not_join_supporter() { + // given + final Long notJoinSupporterId = 1000000L; + final RunnerPostUpdateRequest.SelectSupporter request = new RunnerPostUpdateRequest.SelectSupporter(notJoinSupporterId); + + // when, then + assertThatThrownBy(() -> runnerPostCommandService.updateRunnerPostAppliedSupporter(runnerPostOwner, targetRunnerPost.getId(), request)) + .isInstanceOf(RunnerPostBusinessException.class); + } + + @DisplayName("러너는 자신의 글에 제안한 서포터가 아니면 서포터로 선택할 수 없다.") + @Test + void fail_updateRunnerPostAppliedSupporter_if_not_apply_supporter() { + // given + final Member ditooMember = memberCommandRepository.save(MemberFixture.createDitoo()); + final Supporter notApplySupporter = supporterQueryRepository.save(SupporterFixture.create(ditooMember)); + + final RunnerPostUpdateRequest.SelectSupporter request = new RunnerPostUpdateRequest.SelectSupporter(notApplySupporter.getId()); + + // when, then + assertThatThrownBy(() -> runnerPostCommandService.updateRunnerPostAppliedSupporter(runnerPostOwner, targetRunnerPost.getId(), request)) + .isInstanceOf(RunnerPostBusinessException.class); + } + + @DisplayName("러너는 작성된 글이 아니면 서포터를 선택할 수 없다.") + @Test + void fail_updateRunnerPostAppliedSupporter_if_is_not_written_runnerPost() { + // given + final RunnerPostUpdateRequest.SelectSupporter request = new RunnerPostUpdateRequest.SelectSupporter(applySupporter.getId()); + final Long notWrittenRunnerPostId = 1000000L; + + // when, then + assertThatThrownBy(() -> runnerPostCommandService.updateRunnerPostAppliedSupporter(runnerPostOwner, notWrittenRunnerPostId, request)) + .isInstanceOf(RunnerPostBusinessException.class); + } + + @DisplayName("러너는 자신의 글이 아니면 서포터를 선택할 수 없다.") + @Test + void fail_updateRunnerPostAppliedSupporter_if_is_not_owner_of_runnerPost() { + // given + final Member ditooMember = memberCommandRepository.save(MemberFixture.createDitoo()); + final Runner notOwnerRunner = runnerQueryRepository.save(RunnerFixture.createRunner(ditooMember)); + + final RunnerPostUpdateRequest.SelectSupporter request = new RunnerPostUpdateRequest.SelectSupporter(applySupporter.getId()); + + // when, then + assertThatThrownBy(() -> runnerPostCommandService.updateRunnerPostAppliedSupporter(notOwnerRunner, targetRunnerPost.getId(), request)) + .isInstanceOf(RunnerPostBusinessException.class); + } + + @DisplayName("리뷰가 완료되면 서포터는 게시글의 상태를 리뷰 완료로 변경할 수 있다.") + @Test + void updateRunnerPostReviewStatusDone() { + // given + final IsReviewed isReviewed = IsReviewed.notReviewed(); + final RunnerPost targetRunnerPost = runnerPostQueryRepository.save(RunnerPostFixture.createWithSupporter(runner, assignedSupporter, IN_PROGRESS, isReviewed)); + + // when + runnerPostCommandService.updateRunnerPostReviewStatusDone(targetRunnerPost.getId(), assignedSupporter); + + // then + final Optional maybeRunnerPost = runnerPostQueryRepository.findById(targetRunnerPost.getId()); + assertThat(maybeRunnerPost).isPresent(); + final RunnerPost actualRunnerPost = maybeRunnerPost.get(); + assertThat(actualRunnerPost.getReviewStatus()).isEqualTo(ReviewStatus.DONE); + } + + @DisplayName("없는 게시글의 상태를 리뷰 완료로 변경할 수 없다.") + @Test + void fail_updateRunnerPostReviewStatusDone_if_invalid_runnerPostId() { + // given + final IsReviewed isReviewed = IsReviewed.notReviewed(); + runnerPostQueryRepository.save(RunnerPostFixture.createWithSupporter(runner, assignedSupporter, IN_PROGRESS, isReviewed)); + final Long unsavedRunnerPostId = 100000L; + + // when, then + assertThatThrownBy(() -> runnerPostCommandService.updateRunnerPostReviewStatusDone(unsavedRunnerPostId, assignedSupporter)) + .isInstanceOf(RunnerPostBusinessException.class); + } + + @DisplayName("서포터가 배정 되지 않은 게시글의 상태를 리뷰 완료로 변경할 수 없다.") + @Test + void fail_updateRunnerPostReviewStatusDone_if_supporter_is_null() { + // given + final IsReviewed isReviewed = IsReviewed.notReviewed(); + final RunnerPost targetRunnerPost = runnerPostQueryRepository.save(RunnerPostFixture.createWithSupporter(runner, null, IN_PROGRESS, isReviewed)); + + // when, then + assertThatThrownBy(() -> runnerPostCommandService.updateRunnerPostReviewStatusDone(targetRunnerPost.getId(), assignedSupporter)) + .isInstanceOf(RunnerPostBusinessException.class); + } + + @DisplayName("다른 서포터가 리뷰 중인 게시글의 상태를 리뷰 완료로 변경할 수 없다.") + @Test + void fail_updateRunnerPostReviewStatusDone_if_different_supporter_is_assigned() { + // given + final IsReviewed isReviewed = IsReviewed.notReviewed(); + final RunnerPost targetRunnerPost = runnerPostQueryRepository.save(RunnerPostFixture.createWithSupporter(runner, assignedSupporter, IN_PROGRESS, isReviewed)); + final Member differentMember = memberCommandRepository.save(MemberFixture.createHyena()); + final Supporter differentSupporter = supporterQueryRepository.save(SupporterFixture.create(differentMember)); + + // when, then + assertThatThrownBy(() -> runnerPostCommandService.updateRunnerPostReviewStatusDone(targetRunnerPost.getId(), differentSupporter)) + .isInstanceOf(RunnerPostBusinessException.class); + } + + @DisplayName("만료된 리뷰 게시글의 상태를 리뷰 완료로 변경할 수 없다.") + @Test + void fail_updateRunnerPostReviewStatusDone_if_reviewStatus_is_overdue() { + // given + final IsReviewed isReviewed = IsReviewed.notReviewed(); + final RunnerPost targetRunnerPost = runnerPostQueryRepository.save(RunnerPostFixture.createWithSupporter(runner, assignedSupporter, OVERDUE, isReviewed)); + + // when, then + assertThatThrownBy(() -> runnerPostCommandService.updateRunnerPostReviewStatusDone(targetRunnerPost.getId(), assignedSupporter)) + .isInstanceOf(RunnerPostDomainException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/service/RunnerPostUpdateApplicantCancelationServiceTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/service/RunnerPostUpdateApplicantCancelationServiceTest.java new file mode 100644 index 000000000..4d50c0ec3 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/service/RunnerPostUpdateApplicantCancelationServiceTest.java @@ -0,0 +1,111 @@ +package touch.baton.domain.runnerpost.command.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.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.SupporterRunnerPost; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.exception.RunnerPostBusinessException; +import touch.baton.domain.runnerpost.command.vo.Deadline; +import touch.baton.domain.runnerpost.command.vo.IsReviewed; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.RunnerFixture; +import touch.baton.fixture.domain.RunnerPostFixture; +import touch.baton.fixture.domain.SupporterFixture; +import touch.baton.fixture.domain.SupporterRunnerPostFixture; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RunnerPostUpdateApplicantCancelationServiceTest extends ServiceTestConfig { + + private RunnerPostCommandService runnerPostCommandService; + + private Supporter applicantSupporter; + private Runner revieweeRunner; + + @BeforeEach + void setUp() { + runnerPostCommandService = new RunnerPostCommandService( + runnerPostCommandRepository, + tagCommandRepository, + supporterCommandRepository, + supporterRunnerPostCommandRepository, + publisher + ); + + final Member applicantMember = memberCommandRepository.save(MemberFixture.createDitoo()); + applicantSupporter = supporterQueryRepository.save(SupporterFixture.create(applicantMember)); + + final Member revieweeMember = memberCommandRepository.save(MemberFixture.createJudy()); + revieweeRunner = runnerQueryRepository.save(RunnerFixture.createRunner(revieweeMember)); + } + + @DisplayName("성공한다.") + @Test + void success() { + // given + final RunnerPost runnerPost = runnerPostQueryRepository.save( + RunnerPostFixture.create( + revieweeRunner, + applicantSupporter, + new Deadline(LocalDateTime.now().plusHours(100)) + )); + final SupporterRunnerPost supporterRunnerPost = SupporterRunnerPostFixture.create(runnerPost, applicantSupporter); + supporterRunnerPostQueryRepository.save(supporterRunnerPost); + + // when + runnerPostCommandService.deleteSupporterRunnerPost(applicantSupporter, runnerPost.getId()); + + // then + assertThat(supporterRunnerPostQueryRepository.findById(supporterRunnerPost.getId())).isNotPresent(); + } + + @DisplayName("RunnerPost 가 존재하지 않으면 실패한다.") + @Test + void fail_when_runnerPost_not_found() { + // given + final RunnerPost runnerPost = runnerPostQueryRepository.save( + RunnerPostFixture.create( + revieweeRunner, + applicantSupporter, + new Deadline(LocalDateTime.now().plusHours(100)) + )); + final SupporterRunnerPost supporterRunnerPost = SupporterRunnerPostFixture.create(runnerPost, applicantSupporter); + supporterRunnerPostQueryRepository.save(supporterRunnerPost); + runnerPostQueryRepository.delete(runnerPost); + + // when, then + assertThatThrownBy(() -> runnerPostCommandService.deleteSupporterRunnerPost(applicantSupporter, runnerPost.getId())) + .isInstanceOf(RunnerPostBusinessException.class); + } + + @DisplayName("RunnerPost 의 리뷰 상태가 대기중이 아니면 실패한다.") + @Test + void fail_when_runnerPost_reviewStatus_is_not_NOT_STARTED() { + // given + final IsReviewed isReviewed = IsReviewed.notReviewed(); + final RunnerPost runnerPost = runnerPostQueryRepository.save( + RunnerPostFixture.create( + revieweeRunner, + applicantSupporter, + new Deadline(LocalDateTime.now().plusHours(100)), + ReviewStatus.IN_PROGRESS, + isReviewed + )); + final SupporterRunnerPost supporterRunnerPost = SupporterRunnerPostFixture.create(runnerPost, applicantSupporter); + supporterRunnerPostQueryRepository.save(supporterRunnerPost); + + + // when, then + assertThatThrownBy(() -> runnerPostCommandService.deleteSupporterRunnerPost(applicantSupporter, runnerPost.getId())) + .isInstanceOf(RunnerPostBusinessException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/vo/CuriousContentsTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/vo/CuriousContentsTest.java new file mode 100644 index 000000000..a3330f476 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/vo/CuriousContentsTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.runnerpost.command.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/command/vo/DeadlineTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/vo/DeadlineTest.java new file mode 100644 index 000000000..cea8dc170 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/vo/DeadlineTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.runnerpost.command.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DeadlineTest { + + @DisplayName("value 가 null 이면 예외가 발생한다.") + @Test + void fail_if_value_is_null() { + assertThatThrownBy(() -> new Deadline(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/vo/ImplementedContentsTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/vo/ImplementedContentsTest.java new file mode 100644 index 000000000..c7ac255a5 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/vo/ImplementedContentsTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.runnerpost.command.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/command/vo/IsReviewedTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/vo/IsReviewedTest.java new file mode 100644 index 000000000..697e42705 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/vo/IsReviewedTest.java @@ -0,0 +1,21 @@ +package touch.baton.domain.runnerpost.command.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/command/vo/PostscriptContentsTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/vo/PostscriptContentsTest.java new file mode 100644 index 000000000..265886439 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/vo/PostscriptContentsTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.runnerpost.command.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/runnerpost/command/vo/PullRequestUrlTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/vo/PullRequestUrlTest.java new file mode 100644 index 000000000..21cb8d768 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/command/vo/PullRequestUrlTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.runnerpost.command.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PullRequestUrlTest { + + @DisplayName("value 가 null 이면 예외가 발생한다.") + @Test + void fail_if_value_is_null() { + assertThatThrownBy(() -> new PullRequestUrl(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/query/repository/RunnerPostPageRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/query/repository/RunnerPostPageRepositoryTest.java new file mode 100644 index 000000000..c1c1a589c --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/query/repository/RunnerPostPageRepositoryTest.java @@ -0,0 +1,501 @@ +package touch.baton.domain.runnerpost.query.repository; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import touch.baton.config.RepositoryTestConfig; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.IsReviewed; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.tag.command.RunnerPostTag; +import touch.baton.domain.tag.command.Tag; +import touch.baton.domain.tag.command.vo.TagReducedName; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.RunnerPostFixture; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Comparator; +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.command.vo.ReviewStatus.*; +import static touch.baton.fixture.vo.DeadlineFixture.deadline; + +class RunnerPostPageRepositoryTest extends RepositoryTestConfig { + + @Autowired + private RunnerPostPageRepository runnerPostPageRepository; + + @DisplayName("러너 게시글을 페이징 조회한다 (중간 페이지 조회)") + @Test + void findByPageInfo() { + // given + final Runner runner = persistRunner(MemberFixture.createDitoo()); + final ReviewStatus[] allReviewStatus = ReviewStatus.values(); + final int persistSize = 30; + final List runnerPostIds = new ArrayList<>(); + for (int i = 0; i < persistSize; i++) { + runnerPostIds.add(persistRunnerPost(runner, allReviewStatus[i % allReviewStatus.length]).getId()); + } + final int lastIndex = persistSize - 1; + final Long previousLastId = runnerPostIds.get(lastIndex); + final int limit = 10; + + em.flush(); + em.close(); + + // when + final List runnerPosts = runnerPostPageRepository.pageByReviewStatusAndTagReducedName(previousLastId, limit, null, null); + runnerPostIds.sort(Comparator.reverseOrder()); + final List expected = runnerPostIds.subList(1, 1 + limit); + + // then + assertSoftly(softly -> { + softly.assertThat(runnerPosts).hasSize(limit); + softly.assertThat(runnerPosts.stream().mapToLong(RunnerPost::getId)) + .isEqualTo(expected); + }); + } + + @DisplayName("러너 게시글을 페이징 조회한다 (첫 페이지 조회)") + @Test + void findLatestByLimit() { + // given + final Runner runner = persistRunner(MemberFixture.createDitoo()); + final ReviewStatus[] allReviewStatus = ReviewStatus.values(); + final int runnerPostCount = 10; + final List runnerPostIds = new ArrayList<>(); + for (int i = 0; i < runnerPostCount; i++) { + runnerPostIds.add(persistRunnerPost(runner, allReviewStatus[i % allReviewStatus.length]).getId()); + } + final int limit = 10; + + em.flush(); + em.close(); + + // when + final List runnerPosts = runnerPostPageRepository.pageByReviewStatusAndTagReducedName(null, limit, null, null); + runnerPostIds.sort(Comparator.reverseOrder()); + final List expected = runnerPostIds; + + // then + assertSoftly(softly -> { + softly.assertThat(runnerPosts).hasSize(limit); + softly.assertThat(runnerPosts.stream().mapToLong(RunnerPost::getId)) + .isEqualTo(expected); + }); + } + + @DisplayName("리뷰 상태로 러너 게시글을 페이징 조회한다 (중간 페이지 조회)") + @Test + void findByPageInfoAndReviewStatus() { + // given + final Runner runner = persistRunner(MemberFixture.createDitoo()); + final ReviewStatus reviewStatus = NOT_STARTED; + final List runnerPostIds = new ArrayList<>(); + final int persistSize = 30; + for (int i = 0; i < persistSize; i++) { + runnerPostIds.add(persistRunnerPost(runner, reviewStatus).getId()); + } + final int lastIndex = persistSize - 1; + final Long previousLastId = runnerPostIds.get(lastIndex); + final int limit = 10; + + em.flush(); + em.close(); + + // when + final List runnerPosts = runnerPostPageRepository.pageByReviewStatusAndTagReducedName(previousLastId, limit, null, reviewStatus); + runnerPostIds.sort(Comparator.reverseOrder()); + final List expected = runnerPostIds.subList(1, 1 + limit); + + // then + assertSoftly(softly -> { + softly.assertThat(runnerPosts).hasSize(limit); + softly.assertThat(runnerPosts.stream().mapToLong(RunnerPost::getId)) + .isEqualTo(expected); + }); + } + + @DisplayName("리뷰 상태로 러너 게시글을 페이징 조회한다 (첫 페이지 조회)") + @Test + void findLatestByLimitAndReviewStatus() { + // given + final Runner runner = persistRunner(MemberFixture.createDitoo()); + final ReviewStatus reviewStatus = NOT_STARTED; + final int runnerPostCount = 10; + final List runnerPostIds = new ArrayList<>(); + for (int i = 0; i < runnerPostCount; i++) { + runnerPostIds.add(persistRunnerPost(runner, reviewStatus).getId()); + } + final int limit = 10; + + em.flush(); + em.close(); + + // when + final List runnerPosts = runnerPostPageRepository.pageByReviewStatusAndTagReducedName(null, limit, null, reviewStatus); + runnerPostIds.sort(Comparator.reverseOrder()); + final List expected = runnerPostIds; + + // then + assertSoftly(softly -> { + softly.assertThat(runnerPosts).hasSize(limit); + softly.assertThat(runnerPosts.stream().mapToLong(RunnerPost::getId)) + .isEqualTo(expected); + }); + } + + @DisplayName("축약된 태그 이름과 리뷰 상태로 러너 게시글을 페이징 조회한다 (중간 페이지 조회)") + @Test + void findByPageInfoAndReviewStatusAndTagReducedName() { + // given + final String tagName = "Javascript"; + final ReviewStatus reviewStatus = NOT_STARTED; + + final Runner runner = persistRunner(MemberFixture.createDitoo()); + final Tag tag = persistTag(tagName); + + final List runnerPostIds = new ArrayList<>(); + final int persistSize = 30; + for (int i = 0; i < persistSize; i++) { + final RunnerPost runnerPost = persistRunnerPost(runner, reviewStatus); + persistRunnerPostTag(runnerPost, tag); + runnerPostIds.add(runnerPost.getId()); + } + final int lastIndex = persistSize - 1; + final Long previousLastId = runnerPostIds.get(lastIndex); + final int limit = 10; + + em.flush(); + em.close(); + + // when + final TagReducedName tagReducedName = TagReducedName.from(tagName); + final List runnerPosts = runnerPostPageRepository.pageByReviewStatusAndTagReducedName(previousLastId, limit, tagReducedName, reviewStatus); + runnerPostIds.sort(Comparator.reverseOrder()); + final List expected = runnerPostIds.subList(1, 1 + limit); + + // then + assertSoftly(softly -> { + softly.assertThat(runnerPosts).hasSize(limit); + softly.assertThat(runnerPosts.stream().mapToLong(RunnerPost::getId)) + .isEqualTo(expected); + }); + } + + @DisplayName("축약된 태그 이름과 리뷰 상태로 러너 게시글을 페이징 조회한다 (첫 페이지 조회)") + @Test + void findLatestByLimitAndTagNameAndReviewStatus() { + // given + final String tagName = "Java"; + final ReviewStatus reviewStatus = NOT_STARTED; + + final Runner runner = persistRunner(MemberFixture.createDitoo()); + final Tag tag = persistTag(tagName); + + final List runnerPostIds = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + final RunnerPost runnerPost = persistRunnerPost(runner, reviewStatus); + persistRunnerPostTag(runnerPost, tag); + runnerPostIds.add(runnerPost.getId()); + } + final int limit = 10; + + em.flush(); + em.close(); + + // when + final TagReducedName tagReducedName = TagReducedName.from(tagName); + final List runnerPosts = runnerPostPageRepository.pageByReviewStatusAndTagReducedName(null, limit, tagReducedName, reviewStatus); + runnerPostIds.sort(Comparator.reverseOrder()); + final List expected = runnerPostIds; + + // then + assertSoftly(softly -> { + softly.assertThat(runnerPosts).hasSize(limit); + softly.assertThat(runnerPosts.stream().mapToLong(RunnerPost::getId)) + .isEqualTo(expected); + }); + } + + @DisplayName("서포터 외래키로 리뷰 상태가 NOT_STARTED 인 러너 게시글을 페이징 조회한다 (중간 페이지 조회)") + @Test + void findByPageInfoAndSupporterIdAndReviewStatus_NOT_STARTED() { + // given + final String tagName = "Javascript"; + final Tag tag = persistTag(tagName); + final Runner runner = persistRunner(MemberFixture.createDitoo()); + final Supporter supporter = persistSupporter(MemberFixture.createEthan()); + + final List runnerPostIds = new ArrayList<>(); + final int persistSize = 30; + for (int i = 0; i < persistSize; i++) { + final RunnerPost runnerPost = persistRunnerPost(runner, NOT_STARTED); + persistRunnerPostTag(runnerPost, tag); + if (runnerPost.getId() % 2 == 0) { + persistApplicant(supporter, runnerPost); + } + runnerPostIds.add(runnerPost.getId()); + } + final int lastIndex = persistSize - 1; + final Long previousLastId = runnerPostIds.get(lastIndex); + final int limit = 10; + + em.flush(); + em.close(); + + // when + final List runnerPosts = runnerPostPageRepository.pageBySupporterIdAndReviewStatusNotStarted(previousLastId, limit, supporter.getId()); + runnerPostIds.sort(Comparator.reverseOrder()); + final List expected = runnerPostIds.stream() + .filter(id -> id % 2 == 0 && id < previousLastId) + .limit(limit) + .toList(); + + // then + assertSoftly(softly -> { + softly.assertThat(runnerPosts).hasSize(limit); + softly.assertThat(runnerPosts.stream().mapToLong(RunnerPost::getId)) + .isEqualTo(expected); + }); + } + + @DisplayName("서포터 외래키로 리뷰 상태가 NOT_STARTED 인 러너 게시글을 페이징 조회한다 (첫 페이지 조회)") + @Test + void findLatestByLimitAndSupporterIdAndReviewStatus_NOT_STARTED() { + // given + final String tagName = "Java"; + final Tag tag = persistTag(tagName); + final Runner runner = persistRunner(MemberFixture.createDitoo()); + final Supporter supporter = persistSupporter(MemberFixture.createEthan()); + + final List runnerPostIds = new ArrayList<>(); + final int persistSize = 30; + for (int i = 0; i < persistSize; i++) { + final RunnerPost runnerPost = persistRunnerPost(runner, NOT_STARTED); + persistRunnerPostTag(runnerPost, tag); + if (runnerPost.getId() % 2 == 0) { + persistApplicant(supporter, runnerPost); + } + runnerPostIds.add(runnerPost.getId()); + } + final int limit = 10; + + em.flush(); + em.close(); + + // when + final List runnerPosts = runnerPostPageRepository.pageBySupporterIdAndReviewStatusNotStarted(null, limit, supporter.getId()); + runnerPostIds.sort(Comparator.reverseOrder()); + final List expected = runnerPostIds.stream() + .filter(id -> id % 2 == 0) + .limit(limit) + .toList(); + + // then + assertSoftly(softly -> { + softly.assertThat(runnerPosts).hasSize(limit); + softly.assertThat(runnerPosts.stream().mapToLong(RunnerPost::getId)) + .isEqualTo(expected); + }); + } + + @DisplayName("서포터 외래키로 리뷰 상태가 IN_PROGRESS 인 러너 게시글을 페이징 조회한다 (중간 페이지 조회)") + @Test + void findByPageInfoAndSupporterIdAndReviewStatus_IN_PROGRESS() { + // given + final String tagName = "Javascript"; + final Tag tag = persistTag(tagName); + final Runner runner = persistRunner(MemberFixture.createDitoo()); + final Supporter supporter = persistSupporter(MemberFixture.createEthan()); + + final List runnerPostIds = new ArrayList<>(); + final int persistSize = 30; + for (int i = 0; i < persistSize; i++) { + final RunnerPost runnerPost = persistRunnerPost(runner, NOT_STARTED); + persistRunnerPostTag(runnerPost, tag); + if (runnerPost.getId() % 2 == 0) { + persistApplicant(supporter, runnerPost); + runnerPost.assignSupporter(supporter); + } + runnerPostIds.add(runnerPost.getId()); + } + final int lastIndex = persistSize - 1; + final Long previousLastId = runnerPostIds.get(lastIndex); + final int limit = 10; + + em.flush(); + em.close(); + + // when + final List runnerPosts = runnerPostPageRepository.pageBySupporterIdAndReviewStatus(previousLastId, limit, supporter.getId(), IN_PROGRESS); + runnerPostIds.sort(Comparator.reverseOrder()); + final List expected = runnerPostIds.stream() + .filter(id -> id % 2 == 0 && id < previousLastId) + .limit(limit) + .toList(); + + // then + assertSoftly(softly -> { + softly.assertThat(runnerPosts).hasSize(limit); + softly.assertThat(runnerPosts.stream().mapToLong(RunnerPost::getId)) + .isEqualTo(expected); + }); + } + + @DisplayName("서포터 외래키로 리뷰 상태가 DONE 인 러너 게시글을 페이징 조회한다 (첫 페이지 조회)") + @Test + void findLatestByLimitAndSupporterIdAndReviewStatus_DONE() { + // given + final String tagName = "Java"; + final Tag tag = persistTag(tagName); + final Runner runner = persistRunner(MemberFixture.createDitoo()); + final Supporter supporter = persistSupporter(MemberFixture.createEthan()); + + final List runnerPostIds = new ArrayList<>(); + final int persistSize = 30; + for (int i = 0; i < persistSize; i++) { + final RunnerPost runnerPost = persistRunnerPost(runner, NOT_STARTED); + persistRunnerPostTag(runnerPost, tag); + if (runnerPost.getId() % 2 == 0) { + persistApplicant(supporter, runnerPost); + runnerPost.assignSupporter(supporter); + runnerPost.finishReview(); + } + runnerPostIds.add(runnerPost.getId()); + } + final int limit = 10; + + em.flush(); + em.close(); + + // when + final List runnerPosts = runnerPostPageRepository.pageBySupporterIdAndReviewStatus(null, limit, supporter.getId(), DONE); + runnerPostIds.sort(Comparator.reverseOrder()); + final List expected = runnerPostIds.stream() + .filter(id -> id % 2 == 0) + .limit(limit) + .toList(); + + // then + assertSoftly(softly -> { + softly.assertThat(runnerPosts).hasSize(limit); + softly.assertThat(runnerPosts.stream().mapToLong(RunnerPost::getId)) + .isEqualTo(expected); + }); + } + + @DisplayName("러너 외래키로 리뷰 상태가 NOT_STARTED 인 러너 게시글을 페이징 조회한다 (중간 페이지 조회)") + @Test + void findByPageInfoAndRunnerIdAndReviewStatus_NOT_STARTED() { + // given + final String tagName = "Javascript"; + final Tag tag = persistTag(tagName); + final Runner runner = persistRunner(MemberFixture.createDitoo()); + + final List runnerPostIds = new ArrayList<>(); + final int persistSize = 30; + for (int i = 0; i < persistSize; i++) { + final RunnerPost runnerPost = persistRunnerPost(runner, NOT_STARTED); + persistRunnerPostTag(runnerPost, tag); + runnerPostIds.add(runnerPost.getId()); + + } + final int lastIndex = persistSize - 1; + final Long previousLastId = runnerPostIds.get(lastIndex); + final int limit = 10; + + em.flush(); + em.close(); + + // when + final List runnerPosts = runnerPostPageRepository.pageByRunnerIdAndReviewStatus(previousLastId, limit, runner.getId(), NOT_STARTED); + runnerPostIds.sort(Comparator.reverseOrder()); + final List expected = runnerPostIds.stream() + .filter(id -> id < previousLastId) + .limit(limit) + .toList(); + + // then + assertSoftly(softly -> { + softly.assertThat(runnerPosts).hasSize(limit); + softly.assertThat(runnerPosts.stream().mapToLong(RunnerPost::getId)) + .isEqualTo(expected); + }); + } + + @DisplayName("러너 외래키로 리뷰 상태가 OVERDUE 인 러너 게시글을 페이징 조회한다 (첫 페이지 조회)") + @Test + void findLatestByLimitAndRunnerIdAndReviewStatus_OVERDUE() { + // given + final String tagName = "Java"; + final Tag tag = persistTag(tagName); + final Runner runner = persistRunner(MemberFixture.createDitoo()); + + final List runnerPostIds = new ArrayList<>(); + final int persistSize = 30; + for (int i = 0; i < persistSize; i++) { + final RunnerPost runnerPost = persistRunnerPost(runner, OVERDUE); + persistRunnerPostTag(runnerPost, tag); + runnerPostIds.add(runnerPost.getId()); + } + final int limit = 10; + + em.flush(); + em.close(); + + // when + final List runnerPosts = runnerPostPageRepository.pageByRunnerIdAndReviewStatus(null, limit, runner.getId(), OVERDUE); + runnerPostIds.sort(Comparator.reverseOrder()); + final List expected = runnerPostIds.stream() + .limit(limit) + .toList(); + + // then + assertSoftly(softly -> { + softly.assertThat(runnerPosts).hasSize(limit); + softly.assertThat(runnerPosts.stream().mapToLong(RunnerPost::getId)) + .isEqualTo(expected); + }); + } + + @DisplayName("RunnerPost 목록으로 RunnerPostTag 목록을 조회한다.") + @Test + void findRunnerPostTagsByRunnerPosts() { + // given + final Tag tagReact = persistTag("react"); + final Tag tagJava = persistTag("java"); + + final List runnerPosts = new ArrayList<>(); + final List expected = new ArrayList<>(); + + for (int i = 0; i < 10; i++) { + final Runner runner = persistRunner(MemberFixture.createDitoo()); + final RunnerPost runnerPost = persistRunnerPost(runner); + final RunnerPostTag runnerPostTagReact = persistRunnerPostTag(runnerPost, tagReact); + final RunnerPostTag runnerPostTagJava = persistRunnerPostTag(runnerPost, tagJava); + runnerPosts.add(runnerPost); + expected.addAll(List.of(runnerPostTagReact, runnerPostTagJava)); + } + + em.flush(); + em.close(); + + // when + final List actual = runnerPostPageRepository.findRunnerPostTagsByRunnerPosts(runnerPosts); + + // then + assertThat(actual).isEqualTo(expected); + } + + private RunnerPost persistRunnerPost(final Runner runner, final ReviewStatus reviewStatus) { + final RunnerPost runnerPost = RunnerPostFixture.create(runner, deadline(LocalDateTime.now().plusHours(100)), reviewStatus, IsReviewed.notReviewed()); + em.persist(runnerPost); + return runnerPost; + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/query/repository/RunnerPostQueryRepositoryReadTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/query/repository/RunnerPostQueryRepositoryReadTest.java new file mode 100644 index 000000000..d676f869b --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/query/repository/RunnerPostQueryRepositoryReadTest.java @@ -0,0 +1,117 @@ +package touch.baton.domain.runnerpost.query.repository; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import touch.baton.config.RepositoryTestConfig; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.IsReviewed; +import touch.baton.domain.tag.command.RunnerPostTag; +import touch.baton.domain.tag.command.Tag; +import touch.baton.domain.tag.query.repository.RunnerPostTagQueryRepository; +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.TagFixture; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static touch.baton.domain.runnerpost.command.vo.ReviewStatus.NOT_STARTED; +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; + +class RunnerPostQueryRepositoryReadTest extends RepositoryTestConfig { + + @Autowired + private RunnerPostQueryRepository runnerPostQueryRepository; + + @Autowired + private RunnerPostTagQueryRepository runnerPostTagQueryRepository; + + @DisplayName("RunnerPost 식별자로 RunnerPostTag 목록을 조회할 때 Tag 가 있으면 조회된다.") + @Test + void findRunnerPostTagsById_exist() { + // given + final Member ditoo = MemberFixture.createDitoo(); + em.persist(ditoo); + final Runner runner = RunnerFixture.createRunner(ditoo); + em.persist(runner); + + final RunnerPost runnerPost = RunnerPostFixture.create(title("제 코드를 리뷰해주세요"), + implementedContents("제 코드의 내용은 이렇습니다."), + curiousContents("저는 이것이 궁금합니다."), + postscriptContents("잘 부탁드립니다."), + pullRequestUrl("https://"), + deadline(LocalDateTime.now().plusHours(10)), + watchedCount(0), + NOT_STARTED, + IsReviewed.notReviewed(), + runner, + null, + runnerPostTags(new ArrayList<>())); + runnerPostQueryRepository.save(runnerPost); + + final Tag java = TagFixture.createJava(); + em.persist(java); + final Tag spring = TagFixture.createSpring(); + 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 = runnerPostTagQueryRepository.joinTagByRunnerPostId(runnerPost.getId()); + + // then + assertThat(expected).containsExactly(javaRunnerPostTag, springRunnerPostTag); + } + + @DisplayName("Runner 식별자로 RunnerPost 목록을 조회한다.") + @Test + void findByRunnerId() { + // given + final Member ditoo = MemberFixture.createDitoo(); + em.persist(ditoo); + final Runner runner = RunnerFixture.createRunner(ditoo); + em.persist(runner); + + em.flush(); + em.close(); + + final RunnerPost runnerPost = RunnerPostFixture.create(title("제 코드를 리뷰해주세요"), + implementedContents("제 코드의 내용은 이렇습니다."), + curiousContents("저는 이것이 궁금합니다."), + postscriptContents("잘 부탁드립니다."), + pullRequestUrl("https://"), + deadline(LocalDateTime.now().plusHours(10)), + watchedCount(0), + NOT_STARTED, + IsReviewed.notReviewed(), + runner, + null, + runnerPostTags(new ArrayList<>())); + runnerPostQueryRepository.save(runnerPost); + + // when + final List actual = runnerPostQueryRepository.findByRunnerId(runner.getId()); + + // then + assertThat(actual).containsExactly(runnerPost); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/query/repository/RunnerPostQueryRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/query/repository/RunnerPostQueryRepositoryTest.java new file mode 100644 index 000000000..f645ad6ba --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/query/repository/RunnerPostQueryRepositoryTest.java @@ -0,0 +1,197 @@ +package touch.baton.domain.runnerpost.query.repository; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import touch.baton.config.RepositoryTestConfig; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.repository.dto.RunnerPostApplicantCountDto; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.fixture.domain.MemberFixture; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class RunnerPostQueryRepositoryTest extends RepositoryTestConfig { + + @Autowired + private RunnerPostQueryRepository runnerPostQueryRepository; + + @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 = runnerPostQueryRepository.countApplicantsByRunnerPostIds(List.of( + runnerPostOne.getId(), + runnerPostTwo.getId(), + runnerPostThree.getId(), + runnerPostFour.getId(), + runnerPostFive.getId(), + runnerPostSix.getId(), + runnerPostOne.getId() + )); + + // then + final List expected = List.of( + new RunnerPostApplicantCountDto(runnerPostTwo.getId(), 2L), + new RunnerPostApplicantCountDto(runnerPostThree.getId(), 1L), + new RunnerPostApplicantCountDto(runnerPostFour.getId(), 0L), + new RunnerPostApplicantCountDto(runnerPostFive.getId(), 0L), + new RunnerPostApplicantCountDto(runnerPostSix.getId(), 0L), + new RunnerPostApplicantCountDto(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 List actual = runnerPostQueryRepository.countApplicantsByRunnerPostIds(List.of( + runnerPostOne.getId(), + runnerPostTwo.getId(), + runnerPostThree.getId(), + runnerPostFour.getId(), + runnerPostFive.getId(), + runnerPostSix.getId() + )); + + // then + final List expected = new ArrayList<>(List.of( + new RunnerPostApplicantCountDto(runnerPostOne.getId(), 3L), + new RunnerPostApplicantCountDto(runnerPostTwo.getId(), 2L), + new RunnerPostApplicantCountDto(runnerPostThree.getId(), 1L), + new RunnerPostApplicantCountDto(runnerPostFour.getId(), 0L), + new RunnerPostApplicantCountDto(runnerPostFive.getId(), 0L), + new RunnerPostApplicantCountDto(runnerPostSix.getId(), 0L) + )); + + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("러너 게시글 식별자값으로 러너 게시글과 서포터, 사용자를 조인하여 조회한다.") + @Test + void joinSupporterByRunnerPostId() { + // given + final Runner hyenaRunner = persistRunner(MemberFixture.createHyena()); + final Supporter ditooSupporter = persistSupporter(MemberFixture.createDitoo()); + final RunnerPost runnerPost = persistRunnerPost(hyenaRunner); + persistApplicant(ditooSupporter, runnerPost); + runnerPost.assignSupporter(ditooSupporter); + + em.flush(); + em.close(); + + // when + final Optional maybeActual = runnerPostQueryRepository.joinSupporterByRunnerPostId(runnerPost.getId()); + + // then + assertSoftly(softly -> { + softly.assertThat(maybeActual).isPresent(); + final RunnerPost actual = maybeActual.get(); + + softly.assertThat(actual.getSupporter()).isEqualTo(ditooSupporter); + softly.assertThat(actual).isEqualTo(runnerPost); + }); + } + + @DisplayName("러너 관련 게시글 개수를 조회하는데 성공한다.") + @Test + void countByRunnerIdAndReviewStatus() { + // given + final Runner runner = persistRunner(MemberFixture.createDitoo()); + final int expected = 5; + for (int i = 0; i < expected; i++) { + persistRunnerPost(runner); + } + + em.flush(); + em.close(); + + // when + final Long actual = runnerPostQueryRepository.countByRunnerIdAndReviewStatus(runner.getId(), ReviewStatus.NOT_STARTED); + + // then + assertThat(actual.intValue()).isEqualTo(expected); + } + + @DisplayName("서포터 관련 게시글 개수를 조회하는데 성공한다.") + @Test + void countBySupporterIdAndReviewStatus() { + // given + final Runner runner = persistRunner(MemberFixture.createEthan()); + final Supporter supporter = persistSupporter(MemberFixture.createDitoo()); + final int expected = 3; + for (int i = 0; i < expected; i++) { + final RunnerPost runnerPost = persistRunnerPost(runner); + runnerPost.assignSupporter(supporter); + runnerPost.finishReview(); + } + + em.flush(); + em.close(); + + // when + final Long actual = runnerPostQueryRepository.countBySupporterIdAndReviewStatus(supporter.getId(), ReviewStatus.DONE); + + // then + assertThat(actual.intValue()).isEqualTo(expected); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/query/service/RunnerPostQueryServiceTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/query/service/RunnerPostQueryServiceTest.java new file mode 100644 index 000000000..be09c19ca --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/query/service/RunnerPostQueryServiceTest.java @@ -0,0 +1,859 @@ +package touch.baton.domain.runnerpost.query.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.common.request.PageParams; +import touch.baton.domain.common.response.PageResponse; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.vo.Company; +import touch.baton.domain.member.command.vo.GithubUrl; +import touch.baton.domain.member.command.vo.ImageUrl; +import touch.baton.domain.member.command.vo.MemberName; +import touch.baton.domain.member.command.vo.OauthId; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.exception.RunnerPostBusinessException; +import touch.baton.domain.runnerpost.command.vo.CuriousContents; +import touch.baton.domain.runnerpost.command.vo.Deadline; +import touch.baton.domain.runnerpost.command.vo.ImplementedContents; +import touch.baton.domain.runnerpost.command.vo.IsReviewed; +import touch.baton.domain.runnerpost.command.vo.PostscriptContents; +import touch.baton.domain.runnerpost.command.vo.PullRequestUrl; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.command.vo.Title; +import touch.baton.domain.runnerpost.command.vo.WatchedCount; +import touch.baton.domain.runnerpost.query.controller.response.RunnerPostResponse; +import touch.baton.domain.tag.command.RunnerPostTag; +import touch.baton.domain.tag.command.RunnerPostTags; +import touch.baton.domain.tag.command.Tag; +import touch.baton.domain.tag.command.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.RunnerTechnicalTagsFixture; +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.ArrayList; +import java.util.Collections; +import java.util.List; + +import static java.time.LocalDateTime.now; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static touch.baton.domain.runnerpost.command.vo.ReviewStatus.IN_PROGRESS; +import static touch.baton.domain.runnerpost.command.vo.ReviewStatus.NOT_STARTED; +import static touch.baton.fixture.vo.DeadlineFixture.deadline; +import static touch.baton.fixture.vo.TagNameFixture.tagName; + +class RunnerPostQueryServiceTest extends ServiceTestConfig { + + private RunnerPostQueryService runnerPostQueryService; + + @BeforeEach + void setUp() { + runnerPostQueryService = new RunnerPostQueryService( + runnerPostQueryRepository, + runnerPostPageRepository, + runnerPostTagQueryRepository, + supporterRunnerPostQueryRepository); + } + + @DisplayName("태그 이름과 리뷰 상태를 조건으로 러너 게시글 첫 페이지 조회에 성공한다.") + @Test + void readRunnerPostByPageInfoAndTagNameAndReviewStatus_firstPage() { + // given + final Member hyenaMember = memberCommandRepository.save(MemberFixture.createHyena()); + final Runner hyenaRunner = runnerQueryRepository.save(RunnerFixture.createRunner(hyenaMember)); + + final Tag javaTag = tagCommandRepository.save(TagFixture.create(tagName("자바"))); + final Tag springTag = tagCommandRepository.save(TagFixture.create(tagName("스프링"))); + + final RunnerPost expectedRunnerPostOne = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now().plusHours(100)), + List.of(javaTag, springTag), + NOT_STARTED + )); + + runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now().minusHours(100)), + List.of(javaTag), + ReviewStatus.OVERDUE + )); + + final RunnerPost expectedRunnerPostTwo = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now().plusHours(100)), + List.of(javaTag), + NOT_STARTED + )); + + runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now().plusHours(100)), + List.of(springTag), + NOT_STARTED + )); + + // when + final PageParams pageParams = new PageParams(null, 10); + final PageResponse actual = runnerPostQueryService.pageRunnerPostByTagNameAndReviewStatus( + javaTag.getTagName().getValue(), + pageParams, + NOT_STARTED + ); + + final List runnerPostTags = List.of( + RunnerPostTagFixture.create(expectedRunnerPostOne, javaTag), + RunnerPostTagFixture.create(expectedRunnerPostOne, springTag), + RunnerPostTagFixture.create(expectedRunnerPostTwo, javaTag) + ); + final PageResponse expected = PageResponse.of( + List.of(RunnerPostResponse.Simple.of(expectedRunnerPostTwo, 0L, runnerPostTags), + RunnerPostResponse.Simple.of(expectedRunnerPostOne, 0L, runnerPostTags)), + pageParams + ); + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("태그 이름과 리뷰 상태를 조건으로 러너 게시글 중간 페이지 조회에 성공한다.") + @Test + void readRunnerPostByPageInfoAndTagNameAndReviewStatus_middlePage() { + // given + final Member hyenaMember = memberCommandRepository.save(MemberFixture.createHyena()); + final Runner hyenaRunner = runnerQueryRepository.save(RunnerFixture.createRunner(hyenaMember)); + + final Tag javaTag = tagCommandRepository.save(TagFixture.create(tagName("자바"))); + final Tag springTag = tagCommandRepository.save(TagFixture.create(tagName("스프링"))); + + final RunnerPost expectedRunnerPostOne = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now().plusHours(100)), + List.of(javaTag, springTag), + NOT_STARTED + )); + + runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now().minusHours(100)), + List.of(javaTag, springTag), + ReviewStatus.OVERDUE + )); + + final RunnerPost expectedRunnerPostTwo = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now().plusHours(100)), + List.of(javaTag), + NOT_STARTED + )); + + runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(now().plusHours(100)), + List.of(springTag), + NOT_STARTED + )); + + final RunnerPost previousRunnerPost = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(now().plusHours(100)), + List.of(javaTag), + NOT_STARTED + )); + + // when + final PageParams pageParams = new PageParams(previousRunnerPost.getId(), 10); + final PageResponse actual = runnerPostQueryService.pageRunnerPostByTagNameAndReviewStatus( + javaTag.getTagName().getValue(), + pageParams, + NOT_STARTED + ); + + final List runnerPostTags = List.of( + RunnerPostTagFixture.create(expectedRunnerPostOne, javaTag), + RunnerPostTagFixture.create(expectedRunnerPostOne, springTag), + RunnerPostTagFixture.create(expectedRunnerPostTwo, javaTag) + ); + final PageResponse expected = PageResponse.of( + List.of(RunnerPostResponse.Simple.of(expectedRunnerPostTwo, 0L, runnerPostTags), + RunnerPostResponse.Simple.of(expectedRunnerPostOne, 0L, runnerPostTags)), + pageParams + ); + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("리뷰 상태를 조건으로 러너 게시글 첫 페이지 조회에 성공한다.") + @Test + void readRunnerPostByPageInfoAndReviewStatus_firstPage() { + // given + final Member hyenaMember = memberCommandRepository.save(MemberFixture.createHyena()); + final Runner hyenaRunner = runnerQueryRepository.save(RunnerFixture.createRunner(hyenaMember)); + + final Tag javaTag = tagCommandRepository.save(TagFixture.create(tagName("자바"))); + final Tag springTag = tagCommandRepository.save(TagFixture.create(tagName("스프링"))); + + final RunnerPost expectedRunnerPostOne = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now().plusHours(100)), + List.of(javaTag, springTag), + NOT_STARTED + )); + + final RunnerPost expectedRunnerPostTwo = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now().plusHours(100)), + List.of(javaTag), + NOT_STARTED + )); + + final RunnerPost expectedRunnerPostThree = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(now().plusHours(100)), + List.of(springTag), + NOT_STARTED + )); + + runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(now().minusHours(100)), + List.of(springTag), + ReviewStatus.OVERDUE + )); + + // when + final PageParams pageParams = new PageParams(null, 10); + final PageResponse actual = runnerPostQueryService.pageRunnerPostByTagNameAndReviewStatus( + null, + pageParams, + NOT_STARTED + ); + + final List runnerPostTags = List.of( + RunnerPostTagFixture.create(expectedRunnerPostOne, javaTag), + RunnerPostTagFixture.create(expectedRunnerPostOne, springTag), + RunnerPostTagFixture.create(expectedRunnerPostTwo, javaTag), + RunnerPostTagFixture.create(expectedRunnerPostThree, springTag) + ); + final PageResponse expected = PageResponse.of( + List.of(RunnerPostResponse.Simple.of(expectedRunnerPostThree, 0L, runnerPostTags), + RunnerPostResponse.Simple.of(expectedRunnerPostTwo, 0L, runnerPostTags), + RunnerPostResponse.Simple.of(expectedRunnerPostOne, 0L, runnerPostTags)), + pageParams + ); + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("리뷰 상태를 조건으로 러너 게시글 중간 페이지 조회에 성공한다.") + @Test + void readRunnerPostByPageInfoAndReviewStatus_middlePage() { + // given + final Member hyenaMember = memberCommandRepository.save(MemberFixture.createHyena()); + final Runner hyenaRunner = runnerQueryRepository.save(RunnerFixture.createRunner(hyenaMember)); + + final Tag javaTag = tagCommandRepository.save(TagFixture.create(tagName("자바"))); + final Tag springTag = tagCommandRepository.save(TagFixture.create(tagName("스프링"))); + + final RunnerPost expectedRunnerPostOne = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now().plusHours(100)), + List.of(javaTag, springTag), + NOT_STARTED + )); + + final RunnerPost expectedRunnerPostTwo = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now().plusHours(100)), + List.of(javaTag), + NOT_STARTED + )); + + final RunnerPost expectedRunnerPostThree = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(now().plusHours(100)), + List.of(springTag), + NOT_STARTED + )); + + runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(now().minusHours(100)), + List.of(springTag), + ReviewStatus.OVERDUE + )); + + final RunnerPost previousRunnerPost = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(now().plusHours(100)), + List.of(springTag), + NOT_STARTED + )); + + // when + final PageParams pageParams = new PageParams(previousRunnerPost.getId(), 10); + final PageResponse actual = runnerPostQueryService.pageRunnerPostByTagNameAndReviewStatus( + null, + pageParams, + NOT_STARTED + ); + + final List runnerPostTags = List.of( + RunnerPostTagFixture.create(expectedRunnerPostOne, javaTag), + RunnerPostTagFixture.create(expectedRunnerPostOne, springTag), + RunnerPostTagFixture.create(expectedRunnerPostTwo, javaTag), + RunnerPostTagFixture.create(expectedRunnerPostThree, springTag) + ); + final PageResponse expected = PageResponse.of( + List.of(RunnerPostResponse.Simple.of(expectedRunnerPostThree, 0L, runnerPostTags), + RunnerPostResponse.Simple.of(expectedRunnerPostTwo, 0L, runnerPostTags), + RunnerPostResponse.Simple.of(expectedRunnerPostOne, 0L, runnerPostTags)), + pageParams + ); + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("태그 이름을 조건으로 러너 게시글 첫 페이지 조회에 성공한다.") + @Test + void readRunnerPostByPageInfoAndTagName_firstPage() { + // given + final Member hyenaMember = memberCommandRepository.save(MemberFixture.createHyena()); + final Runner hyenaRunner = runnerQueryRepository.save(RunnerFixture.createRunner(hyenaMember)); + + final Tag javaTag = tagCommandRepository.save(TagFixture.create(tagName("자바"))); + final Tag springTag = tagCommandRepository.save(TagFixture.create(tagName("스프링"))); + + final RunnerPost expectedRunnerPostOne = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now()), + List.of(javaTag, springTag), + ReviewStatus.OVERDUE + )); + + final RunnerPost expectedRunnerPostTwo = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now().plusHours(100)), + List.of(javaTag), + NOT_STARTED + )); + + runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(now().plusHours(100)), + List.of(springTag), + ReviewStatus.IN_PROGRESS + )); + + // when + final PageParams pageParams = new PageParams(null, 10); + final PageResponse actual = runnerPostQueryService.pageRunnerPostByTagNameAndReviewStatus( + javaTag.getTagName().getValue(), + pageParams, + null + ); + + final List runnerPostTags = List.of( + RunnerPostTagFixture.create(expectedRunnerPostOne, javaTag), + RunnerPostTagFixture.create(expectedRunnerPostOne, springTag), + RunnerPostTagFixture.create(expectedRunnerPostTwo, javaTag) + ); + final PageResponse expected = PageResponse.of( + List.of(RunnerPostResponse.Simple.of(expectedRunnerPostTwo, 0L, runnerPostTags), + RunnerPostResponse.Simple.of(expectedRunnerPostOne, 0L, runnerPostTags)), + pageParams + ); + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("태그 이름을 조건으로 러너 게시글 중간 페이지 조회에 성공한다.") + @Test + void readRunnerPostByPageInfoAndTagName_middlePage() { + // given + final Member hyenaMember = memberCommandRepository.save(MemberFixture.createHyena()); + final Runner hyenaRunner = runnerQueryRepository.save(RunnerFixture.createRunner(hyenaMember)); + + final Tag javaTag = tagCommandRepository.save(TagFixture.create(tagName("자바"))); + final Tag springTag = tagCommandRepository.save(TagFixture.create(tagName("스프링"))); + + final RunnerPost expectedRunnerPostOne = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now().plusHours(100)), + List.of(javaTag, springTag), + NOT_STARTED + )); + + final RunnerPost expectedRunnerPostTwo = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now()), + List.of(javaTag), + ReviewStatus.OVERDUE + )); + + runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(now().plusHours(100)), + List.of(springTag), + ReviewStatus.IN_PROGRESS + )); + + final RunnerPost previousRunnerPost = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(now().plusHours(100)), + List.of(javaTag), + NOT_STARTED + )); + + // when + final PageParams pageParams = new PageParams(previousRunnerPost.getId(), 10); + final PageResponse actual = runnerPostQueryService.pageRunnerPostByTagNameAndReviewStatus( + javaTag.getTagName().getValue(), + pageParams, + null + ); + + final List runnerPostTags = List.of( + RunnerPostTagFixture.create(expectedRunnerPostOne, javaTag), + RunnerPostTagFixture.create(expectedRunnerPostOne, springTag), + RunnerPostTagFixture.create(expectedRunnerPostTwo, javaTag) + ); + final PageResponse expected = PageResponse.of( + List.of(RunnerPostResponse.Simple.of(expectedRunnerPostTwo, 0L, runnerPostTags), + RunnerPostResponse.Simple.of(expectedRunnerPostOne, 0L, runnerPostTags)), + pageParams + ); + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("조건 없이 러너 게시글 첫 페이지 조회에 성공한다.") + @Test + void readRunnerPostByPageInfo_firstPage() { + // given + final Member hyenaMember = memberCommandRepository.save(MemberFixture.createHyena()); + final Runner hyenaRunner = runnerQueryRepository.save(RunnerFixture.createRunner(hyenaMember)); + + final Tag javaTag = tagCommandRepository.save(TagFixture.create(tagName("자바"))); + final Tag springTag = tagCommandRepository.save(TagFixture.create(tagName("스프링"))); + + final RunnerPost expectedRunnerPostOne = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now()), + List.of(javaTag, springTag), + ReviewStatus.OVERDUE + )); + + final RunnerPost expectedRunnerPostTwo = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now().plusHours(100)), + List.of(javaTag), + NOT_STARTED + )); + + final RunnerPost expectedRunnerPostThree = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(now().plusHours(100)), + List.of(springTag), + IN_PROGRESS + )); + + // when + final PageParams pageParams = new PageParams(null, 10); + final PageResponse actual = runnerPostQueryService.pageRunnerPostByTagNameAndReviewStatus( + null, + pageParams, + null + ); + + final List runnerPostTags = List.of( + RunnerPostTagFixture.create(expectedRunnerPostOne, javaTag), + RunnerPostTagFixture.create(expectedRunnerPostOne, springTag), + RunnerPostTagFixture.create(expectedRunnerPostTwo, javaTag), + RunnerPostTagFixture.create(expectedRunnerPostThree, springTag) + ); + final PageResponse expected = PageResponse.of( + List.of(RunnerPostResponse.Simple.of(expectedRunnerPostThree, 0L, runnerPostTags), + RunnerPostResponse.Simple.of(expectedRunnerPostTwo, 0L, runnerPostTags), + RunnerPostResponse.Simple.of(expectedRunnerPostOne, 0L, runnerPostTags)), + pageParams + ); + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("조건 없이 러너 게시글 중간 페이지 조회에 성공한다.") + @Test + void readRunnerPostByPageInfo_middlePage() { + // given + final Member hyenaMember = memberCommandRepository.save(MemberFixture.createHyena()); + final Runner hyenaRunner = runnerQueryRepository.save(RunnerFixture.createRunner(hyenaMember)); + + final Tag javaTag = tagCommandRepository.save(TagFixture.create(tagName("자바"))); + final Tag springTag = tagCommandRepository.save(TagFixture.create(tagName("스프링"))); + + final RunnerPost expectedRunnerPostOne = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now().plusHours(100)), + List.of(javaTag, springTag), + NOT_STARTED + )); + + final RunnerPost expectedRunnerPostTwo = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(LocalDateTime.now()), + List.of(javaTag), + ReviewStatus.OVERDUE + )); + + final RunnerPost expectedRunnerPostThree = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(now().plusHours(100)), + List.of(springTag), + IN_PROGRESS + )); + + final RunnerPost previousRunnerPost = runnerPostQueryRepository.save(RunnerPostFixture.create( + hyenaRunner, + deadline(now().plusHours(100)), + List.of(javaTag), + NOT_STARTED + )); + + // when + final PageParams pageParams = new PageParams(previousRunnerPost.getId(), 10); + final PageResponse actual = runnerPostQueryService.pageRunnerPostByTagNameAndReviewStatus( + null, + pageParams, + null + ); + + final List runnerPostTags = List.of( + RunnerPostTagFixture.create(expectedRunnerPostOne, javaTag), + RunnerPostTagFixture.create(expectedRunnerPostOne, springTag), + RunnerPostTagFixture.create(expectedRunnerPostTwo, javaTag), + RunnerPostTagFixture.create(expectedRunnerPostThree, springTag) + ); + final PageResponse expected = PageResponse.of( + List.of(RunnerPostResponse.Simple.of(expectedRunnerPostThree, 0L, runnerPostTags), + RunnerPostResponse.Simple.of(expectedRunnerPostTwo, 0L, runnerPostTags), + RunnerPostResponse.Simple.of(expectedRunnerPostOne, 0L, runnerPostTags)), + pageParams + ); + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("RunnerPost 식별자로 RunnerPost 를 조회한다.") + @Test + void success_findByRunnerPostId() { + // given + final Member member = Member.builder() + .memberName(new MemberName("헤에디주")) + .socialId(new SocialId("testSocialId")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("홍혁준")) + .build(); + memberCommandRepository.save(member); + + final Runner runner = Runner.builder() + .member(member) + .runnerTechnicalTags(RunnerTechnicalTagsFixture.create(new ArrayList<>())) + .build(); + runnerQueryRepository.save(runner); + + final LocalDateTime deadline = now(); + final RunnerPost runnerPost = RunnerPost.builder() + .title(new Title("제 코드 리뷰 좀 해주세요!!")) + .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(); + runnerPostQueryRepository.save(runnerPost); + + final Tag tag = Tag.builder() + .tagName(new TagName("자바")) + .tagReducedName(TagReducedName.from("자바")) + .build(); + tagCommandRepository.save(tag); + + final RunnerPostTag runnerPostTag = RunnerPostTag.builder() + .runnerPost(runnerPost) + .tag(tag) + .build(); + runnerPost.addAllRunnerPostTags(List.of(runnerPostTag)); + + // when + final RunnerPost findRunnerPost = runnerPostQueryService.readByRunnerPostId(runnerPost.getId()); + + // then + assertThat(findRunnerPost) + .usingRecursiveComparison() + .ignoringExpectedNullFields() + .isEqualTo(runnerPost); + } + + @DisplayName("RunnerPost 식별자로 존재하지 않는 RunnerPost 를 조회할 경우 예외가 발생한다.") + @Test + void fail_findByRunnerPostId_if_runner_post_is_null() { + assertThatThrownBy(() -> runnerPostQueryService.readByRunnerPostId(0L)) + .isInstanceOf(RunnerPostBusinessException.class) + .hasMessage("RunnerPost 의 식별자값으로 러너 게시글을 조회할 수 없습니다."); + } + + @DisplayName("Runner 식별자값으로 RunnerPost 를 조회한다.") + @Test + void success_findByRunnerId() { + // given + final Member ditoo = MemberFixture.createDitoo(); + memberCommandRepository.save(ditoo); + final Runner runner = RunnerFixture.createRunner(ditoo); + runnerQueryRepository.save(runner); + final RunnerPost expected = RunnerPostFixture.create(runner, new Deadline(now().plusHours(100))); + runnerPostQueryRepository.save(expected); + + // when + final List actual = runnerPostQueryService.readRunnerPostsByRunnerId(runner.getId()); + + // then + assertSoftly(softly -> { + softly.assertThat(actual).hasSize(1); + softly.assertThat(actual.get(0)).isEqualTo(expected); + }); + } + + @DisplayName("Supporter 외래키와 ReviewStatus 로 러너 게시글을 조회한다. (NOT_STARTED 제외)") + @Test + void readRunnerPostsBySupporterIdAndReviewStatus() { + // given + final Member savedMemberEthan = memberCommandRepository.save(MemberFixture.createEthan()); + final Runner savedRunnerEthan = runnerQueryRepository.save(RunnerFixture.createRunner(savedMemberEthan)); + + final Member savedMemberHyena = memberCommandRepository.save(MemberFixture.createHyena()); + final Supporter savedSupporterHyena = supporterQueryRepository.save(SupporterFixture.create(savedMemberHyena)); + + final RunnerPost runnerPost = RunnerPostFixture.create(savedRunnerEthan, deadline(now().plusHours(100))); + final RunnerPost savedRunnerPost = runnerPostQueryRepository.save(runnerPost); + savedRunnerPost.assignSupporter(savedSupporterHyena); + + supporterRunnerPostQueryRepository.save(SupporterRunnerPostFixture.create(runnerPost, savedSupporterHyena)); + + // when + final PageParams pageParams = new PageParams(null, 10); + final PageResponse actual + = runnerPostQueryService.pageRunnerPostBySupporterIdAndReviewStatus(pageParams, savedSupporterHyena.getId(), IN_PROGRESS); + final PageResponse expected = PageResponse.of( + List.of(RunnerPostResponse.Simple.of(savedRunnerPost, 1L, Collections.emptyList())), + pageParams + ); + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("Supporter 외래키와 ReviewStatus 로 러너 게시글을 조회한다. (NOT_STARTED 인 경우)") + @Test + void readRunnerPostsBySupporterIdAndReviewStatusIs_NOT_STARTED() { + // given + final Member member = memberCommandRepository.save(MemberFixture.createEthan()); + final Runner runner = runnerQueryRepository.save(RunnerFixture.createRunner(member)); + + final Member supporterMember = memberCommandRepository.save(MemberFixture.createHyena()); + final Supporter supporter = supporterQueryRepository.save(SupporterFixture.create(supporterMember)); + + final RunnerPost runnerPost = RunnerPostFixture.create(runner, deadline(now().plusHours(100))); + final RunnerPost savedRunnerPost = runnerPostQueryRepository.save(runnerPost); + + supporterRunnerPostQueryRepository.save(SupporterRunnerPostFixture.create(runnerPost, supporter)); + + // when + final PageParams pageParams = new PageParams(null, 10); + final PageResponse actual + = runnerPostQueryService.pageRunnerPostBySupporterIdAndReviewStatus(pageParams, supporter.getId(), NOT_STARTED); + final PageResponse expected = PageResponse.of( + List.of(RunnerPostResponse.Simple.of(savedRunnerPost, 1L, Collections.emptyList())), + pageParams + ); + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("Runner 외래키와 ReviewStatus 로 러너 게시글을 조회한다.") + @Test + void readRunnerPostsByRunnerIdAndReviewStatus() { + // given + final Member member = memberCommandRepository.save(MemberFixture.createEthan()); + final Runner runner = runnerQueryRepository.save(RunnerFixture.createRunner(member)); + + final RunnerPost runnerPost = RunnerPostFixture.create(runner, deadline(now().plusHours(100))); + final RunnerPost savedRunnerPost = runnerPostQueryRepository.save(runnerPost); + + // when + final PageParams pageParams = new PageParams(null, 10); + final PageResponse actual + = runnerPostQueryService.pageRunnerPostByRunnerIdAndReviewStatus(pageParams, runner.getId(), NOT_STARTED); + final PageResponse expected = PageResponse.of( + List.of(RunnerPostResponse.SimpleByRunner.of(savedRunnerPost, 0L, Collections.emptyList())), + pageParams + ); + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("Member 가 RunnerPost 에 지원한 이력이 있을 경우 true 를 반환한다.") + @Test + void existsRunnerPostApplicantByRunnerPostIdAndMemberId() { + // given + final Member savedMemberDitoo = memberCommandRepository.save(MemberFixture.createDitoo()); + final Runner savedRunnerDitoo = runnerQueryRepository.save(RunnerFixture.createRunner(savedMemberDitoo)); + + final Member savedMemberHyena = memberCommandRepository.save(MemberFixture.createHyena()); + final Supporter savedSupporterHyena = supporterQueryRepository.save(SupporterFixture.create(savedMemberHyena)); + + final RunnerPost savedRunnerPost = runnerPostQueryRepository.save(RunnerPostFixture.create(savedRunnerDitoo, new Deadline(now().plusHours(100)))); + + supporterRunnerPostQueryRepository.save(SupporterRunnerPostFixture.create(savedRunnerPost, savedSupporterHyena)); + + // when + final boolean isApplicantHistoryExist = runnerPostQueryService.existsRunnerPostApplicantByRunnerPostIdAndMemberId( + savedRunnerPost.getId(), + savedMemberHyena.getId() + ); + + // then + assertThat(isApplicantHistoryExist).isTrue(); + } + + @DisplayName("Member 가 RunnerPost 에 지원한 이력을 조회할 때 RunnerPost 자체가 없으면 false 를 반환한다.") + @Test + void existsRunnerPostApplicantByRunnerPostIdAndMemberId_if_runnerPost_is_not_exist_then_return_false() { + // given + final Member savedMemberDitoo = memberCommandRepository.save(MemberFixture.createDitoo()); + final Runner savedRunnerDitoo = runnerQueryRepository.save(RunnerFixture.createRunner(savedMemberDitoo)); + + final Member savedMemberHyena = memberCommandRepository.save(MemberFixture.createHyena()); + + // when + final Long notExistRunnerPostId = -1L; + final boolean isApplicantHistoryExist = runnerPostQueryService.existsRunnerPostApplicantByRunnerPostIdAndMemberId( + notExistRunnerPostId, + savedMemberHyena.getId() + ); + + // then + assertThat(isApplicantHistoryExist).isFalse(); + } + + @DisplayName("Member 가 RunnerPost 에 지원한 이력이 없을 경우 false 를 반환한다.") + @Test + void existsRunnerPostApplicantByRunnerPostIdAndMemberId_if_member_is_not_exist_then_return_false() { + // given + final Member savedMemberDitoo = memberCommandRepository.save(MemberFixture.createDitoo()); + final Runner savedRunnerDitoo = runnerQueryRepository.save(RunnerFixture.createRunner(savedMemberDitoo)); + + final RunnerPost savedRunnerPost = runnerPostQueryRepository.save(RunnerPostFixture.create(savedRunnerDitoo, new Deadline(now().plusHours(100)))); + + // when + final Long notExistMemberId = -1L; + final boolean isApplicantHistoryExist = runnerPostQueryService.existsRunnerPostApplicantByRunnerPostIdAndMemberId( + savedRunnerPost.getId(), + notExistMemberId + ); + + // then + assertThat(isApplicantHistoryExist).isFalse(); + } + + @DisplayName("RunnerId 와 ReviewStatus 로 러너 게시글 개수를 조회한다.") + @Test + void countRunnerPostByRunnerIdAndReviewStatus() { + // given + final Member member = memberCommandRepository.save(MemberFixture.createDitoo()); + final Runner runner = runnerQueryRepository.save(RunnerFixture.createRunner(member)); + + final long expected = 3L; + for (long i = 0; i < expected; i++) { + runnerPostCommandRepository.save(RunnerPostFixture.create(runner, new Deadline(now().plusHours(100)))); + } + + // when + final long actual = runnerPostQueryService.countRunnerPostByRunnerIdAndReviewStatus(runner.getId(), NOT_STARTED); + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("SupporterId 로 ReviewStatus 가 NOT_STARTED 인 러너 게시글 개수를 조회한다.") + @Test + void countRunnerPostBySupporterIdAndReviewStatus_NOT_STARTED() { + // given + final Member member = memberCommandRepository.save(MemberFixture.createDitoo()); + final Runner runner = runnerQueryRepository.save(RunnerFixture.createRunner(member)); + final RunnerPost runnerPost = runnerPostCommandRepository.save(RunnerPostFixture.create(runner, new Deadline(now().plusHours(100)))); + + final Member supporterMember = memberCommandRepository.save(MemberFixture.createDitoo()); + final Supporter supporter = supporterQueryRepository.save(SupporterFixture.create(supporterMember)); + + supporterRunnerPostCommandRepository.save(SupporterRunnerPostFixture.create(runnerPost, supporter)); + + // when + final long expected = 1L; + final long actual = runnerPostQueryService.countRunnerPostBySupporterIdAndReviewStatus(supporter.getId(), NOT_STARTED); + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("SupporterId 로 ReviewStatus 가 NOT_STARTED 이 아닌 러너 게시글 개수를 조회한다.") + @Test + void countRunnerPostBySupporterIdAndReviewStatus_except_NOT_STARTED() { + // given + final Member member = memberCommandRepository.save(MemberFixture.createDitoo()); + final Runner runner = runnerQueryRepository.save(RunnerFixture.createRunner(member)); + final RunnerPost runnerPost = runnerPostCommandRepository.save(RunnerPostFixture.create(runner, new Deadline(now().plusHours(100)))); + + final Member supporterMember = memberCommandRepository.save(MemberFixture.createDitoo()); + final Supporter supporter = supporterQueryRepository.save(SupporterFixture.create(supporterMember)); + + supporterRunnerPostCommandRepository.save(SupporterRunnerPostFixture.create(runnerPost, supporter)); + runnerPost.assignSupporter(supporter); + + // when + final long expected = 1L; + final long actual = runnerPostQueryService.countRunnerPostBySupporterIdAndReviewStatus(supporter.getId(), IN_PROGRESS); + + // then + assertThat(actual).isEqualTo(expected); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/supporter/command/SupporterFeedbackTest.java b/backend/baton/src/test/java/touch/baton/domain/supporter/command/SupporterFeedbackTest.java new file mode 100644 index 000000000..067746f8c --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/supporter/command/SupporterFeedbackTest.java @@ -0,0 +1,157 @@ +package touch.baton.domain.supporter.command; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import touch.baton.domain.feedback.command.vo.Description; +import touch.baton.domain.feedback.command.vo.ReviewType; +import touch.baton.domain.feedback.exception.SupporterFeedbackException; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.vo.ReviewCount; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.IsReviewed; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.RunnerFixture; +import touch.baton.fixture.domain.RunnerPostFixture; +import touch.baton.fixture.domain.RunnerPostTagsFixture; +import touch.baton.fixture.domain.SupporterFixture; + +import java.time.LocalDateTime; +import java.util.ArrayList; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static touch.baton.domain.feedback.command.SupporterFeedback.builder; +import static touch.baton.domain.runnerpost.command.vo.ReviewStatus.NOT_STARTED; +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; + +class SupporterFeedbackTest { + + @DisplayName("생성 테스트") + @Nested + class Create { + + private Supporter supporter; + private Runner runner; + private RunnerPost runnerPost; + + @BeforeEach + void setUp() { + supporter = SupporterFixture.create(new ReviewCount(0), + MemberFixture.createEthan(), + new ArrayList<>()); + + runner = RunnerFixture.createRunner(MemberFixture.createDitoo()); + + runnerPost = RunnerPostFixture.create(title("제 코드를 리뷰해주세요"), + implementedContents("제 코드의 내용은 이렇습니다."), + curiousContents("제 궁금증은 이렇습니다."), + postscriptContents("제 참고 사항은 이렇습니다."), + pullRequestUrl("https://"), + deadline(LocalDateTime.now().plusHours(10)), + watchedCount(0), + NOT_STARTED, + IsReviewed.notReviewed(), + runner, + supporter, + RunnerPostTagsFixture.runnerPostTags(new ArrayList<>())); + } + + @DisplayName("성공한다.") + @Test + void success() { + // when, then + assertThatCode(() -> builder() + .reviewType(ReviewType.GOOD) + .description(new Description("무난무난")) + .supporter(supporter) + .runner(runner) + .runnerPost(runnerPost) + .build() + ).doesNotThrowAnyException(); + } + + @DisplayName("reviewType 이 null 이면 실패한다.") + @Test + void fail_if_reviewType_is_null() { + // when, then + assertThatThrownBy(() -> builder() + .reviewType(null) + .description(new Description("무난무난")) + .supporter(supporter) + .runner(runner) + .runnerPost(runnerPost) + .build() + ).isInstanceOf(SupporterFeedbackException.class) + .hasMessage("SupporterFeedback 의 reviewType 은 null 일 수 없습니다."); + } + + @DisplayName("description 이 null 이면 실패한다.") + @Test + void fail_if_description_is_null() { + // when, then + assertThatThrownBy(() -> builder() + .reviewType(ReviewType.GOOD) + .description(null) + .supporter(supporter) + .runner(runner) + .runnerPost(runnerPost) + .build() + ).isInstanceOf(SupporterFeedbackException.class) + .hasMessage("SupporterFeedback 의 description 은 null 일 수 없습니다."); + } + + @DisplayName("supporter 가 null 이면 실패한다.") + @Test + void fail_if_supporter_is_null() { + // when, then + assertThatThrownBy(() -> builder() + .reviewType(ReviewType.GOOD) + .description(new Description("무난무난")) + .supporter(null) + .runner(runner) + .runnerPost(runnerPost) + .build() + ).isInstanceOf(SupporterFeedbackException.class) + .hasMessage("SupporterFeedback 의 supporter 는 null 일 수 없습니다."); + } + + @DisplayName("runner 가 null 이면 실패한다.") + @Test + void fail_if_runner_is_null() { + // when, then + assertThatThrownBy(() -> builder() + .reviewType(ReviewType.GOOD) + .description(new Description("무난무난")) + .supporter(supporter) + .runner(null) + .runnerPost(runnerPost) + .build() + ).isInstanceOf(SupporterFeedbackException.class) + .hasMessage("SupporterFeedback 의 runner 는 null 일 수 없습니다."); + } + + @DisplayName("runnerPost 가 null 이면 실패한다.") + @Test + void fail_if_runnerPost_is_null() { + // when, then + assertThatThrownBy(() -> builder() + .reviewType(ReviewType.GOOD) + .description(new Description("무난무난")) + .supporter(supporter) + .runner(runner) + .runnerPost(null) + .build() + ).isInstanceOf(SupporterFeedbackException.class) + .hasMessage("SupporterFeedback 의 runnerPost 는 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/supporter/command/SupporterTest.java b/backend/baton/src/test/java/touch/baton/domain/supporter/command/SupporterTest.java new file mode 100644 index 000000000..7129b7725 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/supporter/command/SupporterTest.java @@ -0,0 +1,156 @@ +package touch.baton.domain.supporter.command; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.vo.Company; +import touch.baton.domain.member.command.vo.GithubUrl; +import touch.baton.domain.member.command.vo.ImageUrl; +import touch.baton.domain.member.command.vo.Introduction; +import touch.baton.domain.member.command.vo.MemberName; +import touch.baton.domain.member.command.vo.OauthId; +import touch.baton.domain.member.command.vo.ReviewCount; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.member.exception.SupporterDomainException; +import touch.baton.domain.technicaltag.command.SupporterTechnicalTag; +import touch.baton.domain.technicaltag.command.SupporterTechnicalTags; +import touch.baton.domain.technicaltag.command.TechnicalTag; +import touch.baton.fixture.domain.SupporterFixture; +import touch.baton.fixture.domain.SupporterTechnicalTagFixture; +import touch.baton.fixture.domain.TechnicalTagFixture; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class SupporterTest { + + private final Member member = Member.builder() + .memberName(new MemberName("헤에디주")) + .socialId(new SocialId("testSocialId")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("imageUrl")) + .build(); + + @DisplayName("생성 테스트") + @Nested + class Create { + + @DisplayName("성공한다.") + @Test + void success() { + assertThatCode(() -> Supporter.builder() + .reviewCount(new ReviewCount(10)) + .member(member) + .supporterTechnicalTags(new SupporterTechnicalTags(new ArrayList<>())) + .build() + ).doesNotThrowAnyException(); + } + + @DisplayName("member 가 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_member_is_null() { + assertThatThrownBy(() -> Supporter.builder() + .reviewCount(new ReviewCount(10)) + .member(null) + .supporterTechnicalTags(new SupporterTechnicalTags(new ArrayList<>())) + .build() + ).isInstanceOf(SupporterDomainException.class) + .hasMessage("Supporter 의 member 는 null 일 수 없습니다."); + } + + @DisplayName("supporterTechnicalTags 가 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_supporterTechnicalTags_is_null() { + assertThatThrownBy(() -> Supporter.builder() + .reviewCount(new ReviewCount(10)) + .member(member) + .supporterTechnicalTags(null) + .build() + ).isInstanceOf(SupporterDomainException.class) + .hasMessage("Supporter 의 supporterTechnicalTags 는 null 일 수 없습니다."); + } + } + + @DisplayName("supporter 의 technicalTags 를 조회한다.") + @Test + void read_supporterTechnicalTags() { + // given + final Supporter supporter = Supporter.builder() + .reviewCount(new ReviewCount(10)) + .member(member) + .supporterTechnicalTags(new SupporterTechnicalTags(new ArrayList<>())) + .build(); + + final TechnicalTag technicalTag = TechnicalTagFixture.createJava(); + final SupporterTechnicalTag supporterTechnicalTag = SupporterTechnicalTagFixture.create(supporter, technicalTag); + supporter.addAllSupporterTechnicalTags(List.of(supporterTechnicalTag)); + + // when + final List actual = supporter.getSupporterTechnicalTags().getSupporterTechnicalTags(); + + // then + assertSoftly(softly -> { + softly.assertThat(actual).hasSize(1); + softly.assertThat(actual.get(0)).isEqualTo(supporterTechnicalTag); + }); + } + + @DisplayName("수정 테스트") + @Nested + class Update { + + private Supporter supporter; + + @BeforeEach + void setUp() { + supporter = SupporterFixture.create(member); + } + + @DisplayName("이름 수정에 성공한다.") + @Test + void name_success() { + // given + final MemberName UpdatedName = new MemberName("디투"); + + // when + supporter.updateMemberName(UpdatedName); + + // then + assertThat(supporter.getMember().getMemberName()).isEqualTo(UpdatedName); + } + + @DisplayName("소속 수정에 성공한다.") + @Test + void company_success() { + // given + final Company updatedCompany = new Company("넥슨"); + + // when + supporter.updateCompany(updatedCompany); + + // then + assertThat(supporter.getMember().getCompany()).isEqualTo(updatedCompany); + } + + @DisplayName("소개글 수정에 성공한다.") + @Test + void introduction_success() { + // given + final Introduction updatedIntroduction = new Introduction("디투"); + + // when + supporter.updateIntroduction(updatedIntroduction); + + // then + assertThat(supporter.getIntroduction()).isEqualTo(updatedIntroduction); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/supporter/command/repository/SupporterQueryRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/supporter/command/repository/SupporterQueryRepositoryTest.java new file mode 100644 index 000000000..6a47eb423 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/supporter/command/repository/SupporterQueryRepositoryTest.java @@ -0,0 +1,41 @@ +package touch.baton.domain.supporter.command.repository; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import touch.baton.config.RepositoryTestConfig; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.query.repository.SupporterQueryRepository; +import touch.baton.fixture.domain.MemberFixture; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class SupporterQueryRepositoryTest extends RepositoryTestConfig { + + @Autowired + private SupporterQueryRepository supporterQueryRepository; + + @DisplayName("Supporter 식별자값으로 Member 를 패치 조인하여 조회한다.") + @Test + void joinMemberBySupporterId() { + // given + final Supporter savedSupporter = persistSupporter(MemberFixture.createHyena()); + + // when + final Optional maybeSupporter = supporterQueryRepository.joinMemberBySupporterId(savedSupporter.getId()); + + // then + assertAll( + () -> assertThat(maybeSupporter).isPresent(), + () -> assertThat(maybeSupporter.get().getId()).isEqualTo(savedSupporter.getId()), + () -> assertThat(maybeSupporter.get().getIntroduction()).isEqualTo(savedSupporter.getIntroduction()), + () -> assertThat(maybeSupporter.get().getReviewCount()).isEqualTo(savedSupporter.getReviewCount()), + () -> assertThat(maybeSupporter.get().getSupporterTechnicalTags()).isEqualTo(savedSupporter.getSupporterTechnicalTags()), + () -> assertThat(maybeSupporter.get().getMember()).isEqualTo(savedSupporter.getMember()) + ); + + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/supporter/command/repository/SupporterRunnerPostCommandRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/supporter/command/repository/SupporterRunnerPostCommandRepositoryTest.java new file mode 100644 index 000000000..8a1d9a4f6 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/supporter/command/repository/SupporterRunnerPostCommandRepositoryTest.java @@ -0,0 +1,79 @@ +package touch.baton.domain.supporter.command.repository; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import touch.baton.config.RepositoryTestConfig; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.SupporterRunnerPost; +import touch.baton.domain.member.command.repository.SupporterRunnerPostCommandRepository; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.fixture.domain.MemberFixture; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class SupporterRunnerPostCommandRepositoryTest extends RepositoryTestConfig { + + @Autowired + private SupporterRunnerPostCommandRepository supporterRunnerPostCommandRepository; + + @DisplayName("RunnerPost 외래키로 된 SupporterRunnerPost 가 존재하는지 확인한다.") + @Test + void existsByRunnerPostId() { + // given + final Runner runner = persistRunner(MemberFixture.createDitoo()); + final Supporter supporter = persistSupporter(MemberFixture.createHyena()); + final RunnerPost runnerPostOfApplicantExist = persistRunnerPost(runner); + final RunnerPost runnerPostOfApplicantNotExist = persistRunnerPost(runner); + persistApplicant(supporter, runnerPostOfApplicantExist); + + // when + final boolean actualOfExist = supporterRunnerPostCommandRepository.existsByRunnerPostId(runnerPostOfApplicantExist.getId()); + final boolean actualOfNotExist = supporterRunnerPostCommandRepository.existsByRunnerPostId(runnerPostOfApplicantNotExist.getId()); + + // then + assertSoftly(softly -> { + softly.assertThat(actualOfExist).isTrue(); + softly.assertThat(actualOfNotExist).isFalse(); + }); + } + + @DisplayName("RunnerPostId 와 SupporterId 로 존재 유무를 확인할 수 있다.") + @Test + void existsByRunnerPostIdAndSupporterId() { + // given + final Runner runner = persistRunner(MemberFixture.createEthan()); + final RunnerPost runnerPost = persistRunnerPost(runner); + final Supporter supporter = persistSupporter(MemberFixture.createHyena()); + persistApplicant(supporter, runnerPost); + + final Long notSavedRunnerPostId = -1L; + final Long notSavedSupporter = -1L; + + // when, then + assertSoftly(softly -> { + softly.assertThat(supporterRunnerPostCommandRepository.existsByRunnerPostIdAndSupporterId(runnerPost.getId(), supporter.getId())).isTrue(); + softly.assertThat(supporterRunnerPostCommandRepository.existsByRunnerPostIdAndSupporterId(notSavedRunnerPostId, supporter.getId())).isFalse(); + softly.assertThat(supporterRunnerPostCommandRepository.existsByRunnerPostIdAndSupporterId(runnerPost.getId(), notSavedSupporter)).isFalse(); + } + ); + } + + @DisplayName("서포터의 러너 게시글 리뷰 제안을 철회하는데 성공한다") + @Test + void deleteBySupporterAndRunnerPostId() { + // given + final Supporter supporter = persistSupporter(MemberFixture.createDitoo()); + final Runner runner = persistRunner(MemberFixture.createHyena()); + final RunnerPost runnerPost = persistRunnerPost(runner); + final SupporterRunnerPost supporterRunnerPost = persistApplicant(supporter, runnerPost); + + // when + supporterRunnerPostCommandRepository.deleteBySupporterIdAndRunnerPostId(supporterRunnerPost.getId(), runnerPost.getId()); + + // then + assertThat(supporterRunnerPostCommandRepository.findById(runnerPost.getId())).isNotPresent(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/supporter/command/service/SupporterCommandServiceTest.java b/backend/baton/src/test/java/touch/baton/domain/supporter/command/service/SupporterCommandServiceTest.java new file mode 100644 index 000000000..7f6b41747 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/supporter/command/service/SupporterCommandServiceTest.java @@ -0,0 +1,40 @@ +package touch.baton.domain.supporter.command.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.member.command.Member; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.service.SupporterCommandService; +import touch.baton.domain.member.command.service.dto.SupporterUpdateRequest; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.SupporterFixture; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatCode; + + +class SupporterCommandServiceTest extends ServiceTestConfig { + + private SupporterCommandService supporterCommandService; + + @BeforeEach + void setUp() { + supporterCommandService = new SupporterCommandService(technicalTagQueryRepository, supporterTechnicalTagCommandRepository); + } + + @DisplayName("Supporter 정보를 수정한다.") + @Test + void updateSupporter() { + // given + final Member savedMember = memberCommandRepository.save(MemberFixture.createDitoo()); + final Supporter savedSupporter = supporterQueryRepository.save(SupporterFixture.create(savedMember)); + final SupporterUpdateRequest request = new SupporterUpdateRequest("디투랜드", "두나무", "소개글입니다.", List.of("golang", "rust")); + + // when, then + assertThatCode(() -> supporterCommandService.updateSupporter(savedSupporter, request)) + .doesNotThrowAnyException(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/supporter/query/repository/SupporterRunnerPostQueryRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/supporter/query/repository/SupporterRunnerPostQueryRepositoryTest.java new file mode 100644 index 000000000..3f3ec7bd6 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/supporter/query/repository/SupporterRunnerPostQueryRepositoryTest.java @@ -0,0 +1,112 @@ +package touch.baton.domain.supporter.query.repository; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import touch.baton.config.RepositoryTestConfig; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.SupporterRunnerPost; +import touch.baton.domain.member.query.repository.SupporterRunnerPostQueryRepository; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.fixture.domain.MemberFixture; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class SupporterRunnerPostQueryRepositoryTest extends RepositoryTestConfig { + + @Autowired + private SupporterRunnerPostQueryRepository supporterRunnerPostQueryRepository; + + @DisplayName("Member 가 SupporterRunnerPost 에 지원한 이력이 있을 경우 true 를 반환한다.") + @Test + void existsByRunnerPostIdAndMemberId_return_true() { + // given + final Runner runner = persistRunner(MemberFixture.createDitoo()); + final RunnerPost runnerPost = persistRunnerPost(runner); + final Supporter supporter = persistSupporter(MemberFixture.createEthan()); + final SupporterRunnerPost supporterRunnerPost = persistApplicant(supporter, runnerPost); + + supporterRunnerPostQueryRepository.save(supporterRunnerPost); + + // when + final boolean isApplicantHistoryExist = supporterRunnerPostQueryRepository.existsByRunnerPostIdAndMemberId( + runnerPost.getId(), + supporter.getMember().getId() + ); + + // then + assertThat(isApplicantHistoryExist).isTrue(); + } + + @DisplayName("Member 가 SupporterRunnerPost 에 지원한 이력이 없을 경우 false 를 반환한다.") + @Test + void existsByRunnerPostIdAndMemberId_if_supporterRunnerPost_is_not_exist_then_return_false() { + // given + final Runner runner = persistRunner(MemberFixture.createDitoo()); + final RunnerPost runnerPost = persistRunnerPost(runner); + final Supporter supporter = persistSupporter(MemberFixture.createEthan()); + + // when + final boolean isApplicantHistoryNotExist = supporterRunnerPostQueryRepository.existsByRunnerPostIdAndMemberId( + runnerPost.getId(), + supporter.getMember().getId() + ); + + // then + assertThat(isApplicantHistoryNotExist).isFalse(); + } + + @DisplayName("Member 가 SupporterRunnerPost 에 지원한 이력을 조회할 때 RunnerPost 자체가 없으면 false 를 반환한다.") + @Test + void existsByRunnerPostIdAndMemberId_if_runnerPost_is_not_exist_then_return_false() { + // given + final Supporter supporter = persistSupporter(MemberFixture.createDitoo()); + + // when + final Long notExistRunnerPostId = -1L; + final boolean isApplicantHistoryNotExist = supporterRunnerPostQueryRepository.existsByRunnerPostIdAndMemberId( + notExistRunnerPostId, + supporter.getMember().getId() + ); + + // then + assertThat(isApplicantHistoryNotExist).isFalse(); + } + + @DisplayName("RunnerPostId 로 SupporterRunnerPost 목록을 조회한다.") + @Test + void readByRunnerPostId() { + // given + final Runner runner = persistRunner(MemberFixture.createHyena()); + final RunnerPost runnerPost = persistRunnerPost(runner); + final Supporter supporterDitoo = persistSupporter(MemberFixture.createDitoo()); + final SupporterRunnerPost supporterDitooRunnerPost = persistApplicant(supporterDitoo, runnerPost); + final Supporter supporterEthan = persistSupporter(MemberFixture.createEthan()); + final SupporterRunnerPost supporterEthanRunnerPost = persistApplicant(supporterEthan, runnerPost); + + // when + final List actual = supporterRunnerPostQueryRepository.readByRunnerPostId(runnerPost.getId()); + + // then + assertThat(actual).containsExactly(supporterDitooRunnerPost, supporterEthanRunnerPost); + } + + @DisplayName("SupporterId 와 Not Started 인 ReviewStatus 로 RunnerPost 개수를 센다.") + @Test + void countRunnerPostBySupporterIdByReviewStatusNotStarted() { + // given + final Runner runner = persistRunner(MemberFixture.createHyena()); + final RunnerPost runnerPost = persistRunnerPost(runner); + final Supporter supporter = persistSupporter(MemberFixture.createDitoo()); + persistApplicant(supporter, runnerPost); + + // when + final long count = supporterRunnerPostQueryRepository.countRunnerPostBySupporterIdByReviewStatusNotStarted(supporter.getId()); + + // then + assertThat(count).isEqualTo(1); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/supporter/query/service/SupporterQueryServiceTest.java b/backend/baton/src/test/java/touch/baton/domain/supporter/query/service/SupporterQueryServiceTest.java new file mode 100644 index 000000000..1d1b0aead --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/supporter/query/service/SupporterQueryServiceTest.java @@ -0,0 +1,45 @@ +package touch.baton.domain.supporter.query.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.member.command.Member; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.query.service.SupporterQueryService; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.SupporterFixture; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + + +class SupporterQueryServiceTest extends ServiceTestConfig { + + private SupporterQueryService supporterQueryService; + + @BeforeEach + void setUp() { + supporterQueryService = new SupporterQueryService(supporterQueryRepository); + } + + @DisplayName("Supporter 식별자 값으로 Member 패치 조인하여 Supporter 를 조회한다.") + @Test + void readBySupporterId() { + // given + final Member savedMember = memberCommandRepository.save(MemberFixture.createHyena()); + final Supporter savedSupporter = supporterQueryRepository.save(SupporterFixture.create(savedMember)); + + // when + final Supporter foundSupporter = supporterQueryService.readBySupporterId(savedSupporter.getId()); + + // then + assertAll( + () -> assertThat(foundSupporter.getId()).isEqualTo(savedSupporter.getId()), + () -> assertThat(foundSupporter.getIntroduction()).isEqualTo(savedSupporter.getIntroduction()), + () -> assertThat(foundSupporter.getReviewCount()).isEqualTo(savedSupporter.getReviewCount()), + () -> assertThat(foundSupporter.getSupporterTechnicalTags()).isEqualTo(savedSupporter.getSupporterTechnicalTags()), + () -> assertThat(foundSupporter.getMember()).isEqualTo(savedMember) + ); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/tag/command/RunnerPostTagTest.java b/backend/baton/src/test/java/touch/baton/domain/tag/command/RunnerPostTagTest.java new file mode 100644 index 000000000..3742f29ae --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/tag/command/RunnerPostTagTest.java @@ -0,0 +1,120 @@ +package touch.baton.domain.tag.command; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.vo.Company; +import touch.baton.domain.member.command.vo.GithubUrl; +import touch.baton.domain.member.command.vo.ImageUrl; +import touch.baton.domain.member.command.vo.MemberName; +import touch.baton.domain.member.command.vo.OauthId; +import touch.baton.domain.member.command.vo.ReviewCount; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.CuriousContents; +import touch.baton.domain.runnerpost.command.vo.Deadline; +import touch.baton.domain.runnerpost.command.vo.ImplementedContents; +import touch.baton.domain.runnerpost.command.vo.IsReviewed; +import touch.baton.domain.runnerpost.command.vo.PostscriptContents; +import touch.baton.domain.runnerpost.command.vo.PullRequestUrl; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.command.vo.Title; +import touch.baton.domain.runnerpost.command.vo.WatchedCount; +import touch.baton.domain.tag.exception.RunnerPostTagDomainException; +import touch.baton.domain.technicaltag.command.SupporterTechnicalTags; +import touch.baton.fixture.domain.RunnerTechnicalTagsFixture; + +import java.time.LocalDateTime; +import java.util.ArrayList; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RunnerPostTagTest { + + @DisplayName("생성 테스트") + @Nested + class Create { + + private final Member runnerMember = 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("imageUrl")) + .build(); + + private final Member supporterMember = Member.builder() + .memberName(new MemberName("서포터 사용자")) + .socialId(new SocialId("testSocialId")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/pobi")) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("imageUrl")) + .build(); + + private final Runner runner = Runner.builder() + .member(runnerMember) + .runnerTechnicalTags(RunnerTechnicalTagsFixture.create(new ArrayList<>())) + .build(); + + private final Supporter supporter = Supporter.builder() + .reviewCount(new ReviewCount(10)) + .member(supporterMember) + .supporterTechnicalTags(new SupporterTechnicalTags(new ArrayList<>())) + .build(); + + private final RunnerPost runnerPost = RunnerPost.builder() + .title(new Title("JPA 정복")) + .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.newInstance("자바"); + + @DisplayName("성공한다.") + @Test + void success() { + assertThatCode(() -> RunnerPostTag.builder() + .runnerPost(runnerPost) + .tag(tag) + .build() + ).doesNotThrowAnyException(); + } + + @DisplayName("runner post 가 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_runnerPost_is_null() { + assertThatThrownBy(() -> RunnerPostTag.builder() + .runnerPost(null) + .tag(tag) + .build() + ).isInstanceOf(RunnerPostTagDomainException.class) + .hasMessage("RunnerPostTag 의 runnerPost 는 null 일 수 없습니다."); + } + + @DisplayName("tag 가 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_tag_is_null() { + assertThatThrownBy(() -> RunnerPostTag.builder() + .runnerPost(runnerPost) + .tag(null) + .build() + ).isInstanceOf(RunnerPostTagDomainException.class) + .hasMessage("RunnerPostTag 의 tag 는 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/tag/command/RunnerPostTagsTest.java b/backend/baton/src/test/java/touch/baton/domain/tag/command/RunnerPostTagsTest.java new file mode 100644 index 000000000..598c3ee51 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/tag/command/RunnerPostTagsTest.java @@ -0,0 +1,54 @@ +package touch.baton.domain.tag.command; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.vo.Company; +import touch.baton.domain.member.command.vo.GithubUrl; +import touch.baton.domain.member.command.vo.ImageUrl; +import touch.baton.domain.member.command.vo.MemberName; +import touch.baton.domain.member.command.vo.OauthId; +import touch.baton.domain.member.command.vo.SocialId; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.fixture.domain.RunnerTechnicalTagsFixture; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class RunnerPostTagsTest { + + @DisplayName("RunnerPostTags 에 runnerPostTag 를 추가할 수 있다.") + @Test + void addAllRunnerPostTags() { + // given + RunnerPostTags postTags = new RunnerPostTags(new ArrayList<>()); + Member member = 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(); + Runner runner = Runner.builder() + .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 RunnerPostTag runnerPostTag = RunnerPostTag.builder() + .runnerPost(runnerpost) + .tag(Tag.newInstance("Java")) + .build(); + + // when + postTags.addAll(List.of(runnerPostTag)); + + // then + assertThat(postTags.getRunnerPostTags()).hasSize(1); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/tag/command/TagTest.java b/backend/baton/src/test/java/touch/baton/domain/tag/command/TagTest.java new file mode 100644 index 000000000..f2bec9b19 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/tag/command/TagTest.java @@ -0,0 +1,50 @@ +package touch.baton.domain.tag.command; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.tag.command.Tag; +import touch.baton.domain.tag.command.vo.TagReducedName; +import touch.baton.domain.tag.exception.TagDomainException; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class TagTest { + + @DisplayName("생성 테스트") + @Nested + class Create { + + @DisplayName("성공한다.") + @Test + void success() { + assertThatCode(() -> Tag.builder() + .tagName(new TagName("자바")) + .tagReducedName(TagReducedName.from("자바")) + .build() + ).doesNotThrowAnyException(); + } + + @DisplayName("tag name 이 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_tagName_is_null() { + assertThatThrownBy(() -> Tag.builder() + .tagName(null) + .tagReducedName(TagReducedName.from("hello")) + .build() + ).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/command/vo/TagReducedNameTest.java b/backend/baton/src/test/java/touch/baton/domain/tag/command/vo/TagReducedNameTest.java new file mode 100644 index 000000000..a26810c05 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/tag/command/vo/TagReducedNameTest.java @@ -0,0 +1,45 @@ +package touch.baton.domain.tag.command.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/tag/query/repository/RunnerPostTagQueryRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/tag/query/repository/RunnerPostTagQueryRepositoryTest.java new file mode 100644 index 000000000..ba2e2837d --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/tag/query/repository/RunnerPostTagQueryRepositoryTest.java @@ -0,0 +1,57 @@ +package touch.baton.domain.tag.query.repository; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import touch.baton.config.RepositoryTestConfig; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.tag.command.RunnerPostTag; +import touch.baton.domain.tag.command.Tag; +import touch.baton.fixture.domain.MemberFixture; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class RunnerPostTagQueryRepositoryTest extends RepositoryTestConfig { + + @Autowired + private RunnerPostTagQueryRepository runnerPostTagQueryRepository; + + @DisplayName("RunnerPostTag 의 식별자값 목록으로 Tag 목록을 조회한다.") + @Test + void success_joinTagByRunnerPostId() { + // given + final Runner runner = persistRunner(MemberFixture.createDitoo()); + final RunnerPost runnerPost = persistRunnerPost(runner); + final Tag tag = persistTag("java"); + final RunnerPostTag runnerPostTag = persistRunnerPostTag(runnerPost, tag); + + em.flush(); + em.close(); + + // when + final List joinRunnerPostTags = runnerPostTagQueryRepository.joinTagByRunnerPostId(runnerPost.getId()); + + // then + assertThat(joinRunnerPostTags).containsExactly(runnerPostTag); + } + + @DisplayName("RunnerPostTag 의 식별자값 목록이 비어있을 때 빈 컬렉션을 반환한다.") + @Test + void success_joinTagByRunnerPostIds_if_tag_is_empty() { + // given + final Runner runner = persistRunner(MemberFixture.createDitoo()); + final RunnerPost runnerPost = persistRunnerPost(runner); + + em.flush(); + em.close(); + + // when + final List joinRunnerPostTags = runnerPostTagQueryRepository.joinTagByRunnerPostId(runnerPost.getId()); + + // then + assertThat(joinRunnerPostTags).isEmpty(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/tag/query/repository/TagQuerydslRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/tag/query/repository/TagQuerydslRepositoryTest.java new file mode 100644 index 000000000..19c05e202 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/tag/query/repository/TagQuerydslRepositoryTest.java @@ -0,0 +1,103 @@ +package touch.baton.domain.tag.query.repository; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import touch.baton.config.RepositoryTestConfig; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.tag.command.Tag; +import touch.baton.domain.tag.command.vo.TagReducedName; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class TagQuerydslRepositoryTest extends RepositoryTestConfig { + + @Autowired + private TagQuerydslRepository tagQuerydslRepository; + + @DisplayName("축약된 태그 이름으로 태그 목록을 limit 개 축약된 태그 이름 기준 오름차순으로 검색한다.") + @Test + void readTagsByTagReducedName() { + // given + for (int saveTagCount = 1; saveTagCount <= 3; saveTagCount++) { + persistTag("java" + saveTagCount); + persistTag("javascript" + saveTagCount); + persistTag("assertj" + saveTagCount); + } + for (int saveTagCount = 4; saveTagCount <= 6; saveTagCount++) { + persistTag("j ava" + saveTagCount); + persistTag("j avascript" + saveTagCount); + persistTag("a ssertj" + saveTagCount); + } + for (int saveTagCount = 7; saveTagCount <= 10; saveTagCount++) { + persistTag("ja va" + saveTagCount); + persistTag("ja vascript" + saveTagCount); + persistTag("as sertj" + saveTagCount); + } + + em.flush(); + em.close(); + + // when + final List actual = tagQuerydslRepository.findByTagReducedName(TagReducedName.from("j"), 10); + + // then + final List actualSortedTagNames = actual.stream() + .map(Tag::getTagName) + .map(TagName::getValue) + .toList(); + + assertThat(actualSortedTagNames).containsExactly( + "java1", "ja va10", "java2", "java3", "j ava4", "j ava5", "j ava6", "ja va7", "ja va8", "ja va9" + ); + } + + @DisplayName("입력된 TagReducedName 으로 시작하는 태그만 검색한다") + @Test + void success_readTagsByReducedName_when_name_isNotMatched_atAll() { + // given + persistTag("assertj"); + + em.flush(); + em.close(); + + // when + final TagReducedName tagReducedName = TagReducedName.nullableInstance("j"); + final List actual = tagQuerydslRepository.findByTagReducedName(tagReducedName, 10); + + // then + assertThat(actual.isEmpty()).isTrue(); + } + + @DisplayName("입력된 TagReducedName 으로 시작하는 이름을 갖는 태그가 없다면 빈 목록을 반환한다.") + @Test + void success_readTagsByReducedName_when_foundTags_isEmpty() { + // given + persistTag("aaaaaaaaa"); + + em.flush(); + em.close(); + + // when + final TagReducedName tagReducedName = TagReducedName.nullableInstance("b"); + final List actual = tagQuerydslRepository.findByTagReducedName(tagReducedName, 10); + + // then + assertThat(actual.isEmpty()).isTrue(); + } + + @DisplayName("TagReducedName 내부 값이 blank 인 경우 빈 목록을 반환한다.") + @Test + void success_readTagsByReducedName_when_tagReducedNameIsBlank() { + // given + final TagReducedName tagReducedName = TagReducedName.nullableInstance(""); + + // when + final List actual = tagQuerydslRepository.findByTagReducedName(tagReducedName, 10); + + // then + assertThat(actual.isEmpty()).isTrue(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/tag/query/service/TagQueryServiceTest.java b/backend/baton/src/test/java/touch/baton/domain/tag/query/service/TagQueryServiceTest.java new file mode 100644 index 000000000..eba2b23bd --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/tag/query/service/TagQueryServiceTest.java @@ -0,0 +1,77 @@ +package touch.baton.domain.tag.query.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.command.Tag; +import touch.baton.domain.tag.command.vo.TagReducedName; +import touch.baton.fixture.domain.TagFixture; +import touch.baton.fixture.vo.TagNameFixture; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class TagQueryServiceTest extends ServiceTestConfig { + + private TagQueryService tagQueryService; + + @BeforeEach + void setUp() { + tagQueryService = new TagQueryService(tagQuerydslRepository); + } + + @DisplayName("Tag의 이름으로 Tag를 오름차순으로 10개 조회한다.") + @Test + void success_readTagsByReducedName() { + // given + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("ja va"))); + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("jav a1"))); + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("j ava2"))); + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("ja va3"))); + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("jav a4"))); + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("java 5"))); + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("j ava6"))); + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("ja va7"))); + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("jav a8"))); + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("java 9"))); + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("ju ja"))); + + // when + final TagReducedName tagReducedName = TagReducedName.nullableInstance("j a"); + final List actual = tagQueryService.readTagsByReducedName(tagReducedName, 10); + + // 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("TagReducedName 이 null 인 경우 빈 목록을 반환한다.") + @Test + void success_when_TagReducedName_IsNull() { + // given + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("ja va"))); + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("jav a1"))); + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("j ava2"))); + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("ja va3"))); + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("jav a4"))); + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("java 5"))); + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("j ava6"))); + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("ja va7"))); + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("jav a8"))); + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("java 9"))); + tagCommandRepository.save(TagFixture.create(TagNameFixture.tagName("ju ja"))); + + // when + final TagReducedName nullTagReducedName = null; + final List actual = tagQueryService.readTagsByReducedName(nullTagReducedName, 10); + + // then + assertThat(actual).isEmpty(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/technicaltag/SupporterTechnicalTagTest.java b/backend/baton/src/test/java/touch/baton/domain/technicaltag/SupporterTechnicalTagTest.java index 933e8b5f1..be2e1d1c5 100644 --- a/backend/baton/src/test/java/touch/baton/domain/technicaltag/SupporterTechnicalTagTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/technicaltag/SupporterTechnicalTagTest.java @@ -3,10 +3,12 @@ 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.supporter.Supporter; -import touch.baton.domain.supporter.vo.ReviewCount; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.vo.ReviewCount; import touch.baton.domain.tag.exception.SupporterTechnicalTagDomainException; +import touch.baton.domain.technicaltag.command.SupporterTechnicalTag; +import touch.baton.domain.technicaltag.command.TechnicalTag; import touch.baton.fixture.domain.MemberFixture; import touch.baton.fixture.domain.SupporterFixture; import touch.baton.fixture.domain.TechnicalTagFixture; diff --git a/backend/baton/src/test/java/touch/baton/domain/technicaltag/SupporterTechnicalTagsTest.java b/backend/baton/src/test/java/touch/baton/domain/technicaltag/SupporterTechnicalTagsTest.java index cedbf2dc4..d1a79e93b 100644 --- a/backend/baton/src/test/java/touch/baton/domain/technicaltag/SupporterTechnicalTagsTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/technicaltag/SupporterTechnicalTagsTest.java @@ -3,13 +3,17 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import touch.baton.domain.member.Member; -import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.technicaltag.command.SupporterTechnicalTag; +import touch.baton.domain.technicaltag.command.SupporterTechnicalTags; +import touch.baton.domain.technicaltag.command.TechnicalTag; import touch.baton.fixture.domain.MemberFixture; import touch.baton.fixture.domain.SupporterFixture; import touch.baton.fixture.domain.SupporterTechnicalTagFixture; import touch.baton.fixture.domain.TechnicalTagFixture; +import java.util.ArrayList; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -31,7 +35,7 @@ void setUp() { @Test void addAll() { // given - final SupporterTechnicalTags supporterTechnicalTags = new SupporterTechnicalTags(); + final SupporterTechnicalTags supporterTechnicalTags = new SupporterTechnicalTags(new ArrayList<>()); // when supporterTechnicalTags.addAll(List.of(supporterTechnicalTag)); diff --git a/backend/baton/src/test/java/touch/baton/domain/technicaltag/TechnicalTagTest.java b/backend/baton/src/test/java/touch/baton/domain/technicaltag/TechnicalTagTest.java index e7f77a11f..5c65fdf95 100644 --- a/backend/baton/src/test/java/touch/baton/domain/technicaltag/TechnicalTagTest.java +++ b/backend/baton/src/test/java/touch/baton/domain/technicaltag/TechnicalTagTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; import touch.baton.domain.common.vo.TagName; import touch.baton.domain.tag.exception.TechnicalTagDomainException; +import touch.baton.domain.technicaltag.command.TechnicalTag; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/backend/baton/src/test/java/touch/baton/domain/technicaltag/repository/SupporterTechnicalTagQueryRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/technicaltag/repository/SupporterTechnicalTagQueryRepositoryTest.java new file mode 100644 index 000000000..ff06640ed --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/technicaltag/repository/SupporterTechnicalTagQueryRepositoryTest.java @@ -0,0 +1,59 @@ +package touch.baton.domain.technicaltag.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.command.Member; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.technicaltag.command.SupporterTechnicalTag; +import touch.baton.domain.technicaltag.command.TechnicalTag; +import touch.baton.domain.technicaltag.command.repository.SupporterTechnicalTagCommandRepository; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.SupporterFixture; +import touch.baton.fixture.domain.SupporterTechnicalTagFixture; +import touch.baton.fixture.domain.TechnicalTagFixture; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class SupporterTechnicalTagQueryRepositoryTest extends RepositoryTestConfig { + + @Autowired + private SupporterTechnicalTagCommandRepository supporterTechnicalTagCommandRepository; + + @Autowired + private EntityManager em; + + @DisplayName("batch 로 supporter 의 모든 SupporterTechnicalTag 를 삭제한다.") + @Test + void deleteBySupporter() { + // given + final Member member = MemberFixture.createDitoo(); + em.persist(member); + final Supporter supporter = SupporterFixture.create(member); + em.persist(supporter); + final TechnicalTag technicalTag1 = TechnicalTagFixture.createReact(); + final TechnicalTag technicalTag2 = TechnicalTagFixture.createSpring(); + final TechnicalTag technicalTag3 = TechnicalTagFixture.createJava(); + 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); + supporterTechnicalTagCommandRepository.saveAll(savedSupporterTechnicalTags); + em.flush(); + em.close(); + + // when + final int expected = savedSupporterTechnicalTags.size(); + final int actual = supporterTechnicalTagCommandRepository.deleteBySupporter(supporter); + + // then + assertThat(expected).isEqualTo(actual); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/technicaltag/repository/TechnicalTagQueryRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/technicaltag/repository/TechnicalTagQueryRepositoryTest.java new file mode 100644 index 000000000..b8089f77b --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/technicaltag/repository/TechnicalTagQueryRepositoryTest.java @@ -0,0 +1,53 @@ +package touch.baton.domain.technicaltag.repository; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import touch.baton.config.RepositoryTestConfig; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.technicaltag.command.TechnicalTag; +import touch.baton.domain.technicaltag.query.repository.TechnicalTagQueryRepository; +import touch.baton.fixture.domain.TechnicalTagFixture; +import touch.baton.fixture.vo.TagNameFixture; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class TechnicalTagQueryRepositoryTest extends RepositoryTestConfig { + + @Autowired + private TechnicalTagQueryRepository technicalTagQueryRepository; + + @DisplayName("TagName 으로 TechnicalTag 를 검색할 때 TechnicalTag 가 존재하면 검색된다.") + @Test + void findByName_ifPresent() { + // given + final TagName tagName = TagNameFixture.tagName("java"); + final TechnicalTag expected = technicalTagQueryRepository.save(TechnicalTagFixture.create(tagName)); + technicalTagQueryRepository.flush(); + + // when + final Optional actual = technicalTagQueryRepository.findByTagName(tagName); + + // then + assertSoftly(softly -> { + softly.assertThat(actual).isPresent(); + softly.assertThat(expected).isEqualTo(actual.get()); + }); + } + + @DisplayName("TagName 으로 TechnicalTag 를 검색할 때 TechnicalTag 가 존재하지 않으면 검색되지 않는다.") + @Test + void findByName_ifNotPresent() { + // given + final TagName tagName = new TagName("java"); + + // when + final Optional actual = technicalTagQueryRepository.findByTagName(tagName); + + // then + assertThat(actual).isNotPresent(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/MemberFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/MemberFixture.java index c5c8bb076..feeeec77c 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/domain/MemberFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/MemberFixture.java @@ -1,12 +1,12 @@ package touch.baton.fixture.domain; -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.member.command.Member; +import touch.baton.domain.member.command.vo.Company; +import touch.baton.domain.member.command.vo.GithubUrl; +import touch.baton.domain.member.command.vo.ImageUrl; +import touch.baton.domain.member.command.vo.MemberName; +import touch.baton.domain.member.command.vo.OauthId; +import touch.baton.domain.member.command.vo.SocialId; import static touch.baton.fixture.vo.CompanyFixture.company; import static touch.baton.fixture.vo.GithubUrlFixture.githubUrl; diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/NotificationFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/NotificationFixture.java new file mode 100644 index 000000000..8b0c84fa5 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/NotificationFixture.java @@ -0,0 +1,27 @@ +package touch.baton.fixture.domain; + +import touch.baton.domain.member.command.Member; +import touch.baton.domain.notification.command.Notification; +import touch.baton.domain.notification.command.vo.IsRead; +import touch.baton.domain.notification.command.vo.NotificationReferencedId; + +import static touch.baton.domain.notification.command.vo.NotificationType.RUNNER_POST; +import static touch.baton.fixture.vo.NotificationMessageFixture.notificationMessage; +import static touch.baton.fixture.vo.NotificationTitleFixture.notificationTitle; + +public abstract class NotificationFixture { + + private NotificationFixture() { + } + + public static Notification create(final Member targetMember, final NotificationReferencedId notificationReferencedId) { + return Notification.builder() + .notificationTitle(notificationTitle("테스트용 알림 제목")) + .notificationMessage(notificationMessage("테스트용 알림 내용")) + .notificationType(RUNNER_POST) + .notificationReferencedId(notificationReferencedId) + .isRead(IsRead.asUnRead()) + .member(targetMember) + .build(); + } +} 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 index 466b815fe..b6bedd1b9 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/domain/RefreshTokenFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/RefreshTokenFixture.java @@ -1,9 +1,9 @@ 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; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.oauth.command.token.ExpireDate; +import touch.baton.domain.oauth.command.token.RefreshToken; +import touch.baton.domain.oauth.command.token.Token; public abstract class RefreshTokenFixture { diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerFixture.java index c4ddf38a9..8582a582f 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerFixture.java @@ -1,11 +1,11 @@ package touch.baton.fixture.domain; -import touch.baton.domain.common.vo.Introduction; -import touch.baton.domain.member.Member; -import touch.baton.domain.runner.Runner; -import touch.baton.domain.technicaltag.RunnerTechnicalTag; -import touch.baton.domain.technicaltag.RunnerTechnicalTags; -import touch.baton.domain.technicaltag.TechnicalTag; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.vo.Introduction; +import touch.baton.domain.technicaltag.command.RunnerTechnicalTag; +import touch.baton.domain.technicaltag.command.RunnerTechnicalTags; +import touch.baton.domain.technicaltag.command.TechnicalTag; import java.util.ArrayList; import java.util.List; 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 bae8fc7c1..59d4cd25f 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,20 +1,20 @@ package touch.baton.fixture.domain; -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; -import touch.baton.domain.tag.RunnerPostTag; -import touch.baton.domain.tag.RunnerPostTags; -import touch.baton.domain.tag.Tag; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.runnerpost.command.vo.CuriousContents; +import touch.baton.domain.runnerpost.command.vo.Deadline; +import touch.baton.domain.runnerpost.command.vo.ImplementedContents; +import touch.baton.domain.runnerpost.command.vo.IsReviewed; +import touch.baton.domain.runnerpost.command.vo.PostscriptContents; +import touch.baton.domain.runnerpost.command.vo.PullRequestUrl; +import touch.baton.domain.runnerpost.command.vo.ReviewStatus; +import touch.baton.domain.runnerpost.command.vo.Title; +import touch.baton.domain.runnerpost.command.vo.WatchedCount; +import touch.baton.domain.tag.command.RunnerPostTag; +import touch.baton.domain.tag.command.RunnerPostTags; +import touch.baton.domain.tag.command.Tag; import touch.baton.fixture.vo.DeadlineFixture; import java.time.LocalDateTime; @@ -93,7 +93,7 @@ public static RunnerPost create(final Runner runner, .build(); } - public static RunnerPost create(final Runner runner, final Deadline deadline, List tags) { + public static RunnerPost create(final Runner runner, final Deadline deadline, final List tags) { final RunnerPost runnerPost = RunnerPost.builder() .title(new Title("테스트 제목")) .implementedContents(new ImplementedContents("테스트 내용")) @@ -118,6 +118,35 @@ public static RunnerPost create(final Runner runner, final Deadline deadline, Li return runnerPost; } + public static RunnerPost create(final Runner runner, + final Deadline deadline, + final List tags, + final ReviewStatus reviewStatus + ) { + final RunnerPost runnerPost = RunnerPost.builder() + .title(new Title("테스트 제목")) + .implementedContents(new ImplementedContents("테스트 내용")) + .curiousContents(new CuriousContents("테스트 궁금 점")) + .postscriptContents(new PostscriptContents("테스트 참고 사항")) + .pullRequestUrl(new PullRequestUrl("https://테스트")) + .deadline(deadline) + .watchedCount(new WatchedCount(0)) + .reviewStatus(reviewStatus) + .isReviewed(IsReviewed.notReviewed()) + .runner(runner) + .supporter(null) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .build(); + + final List runnerPostTags = tags.stream() + .map(tag -> RunnerPostTagFixture.create(runnerPost, tag)) + .toList(); + + runnerPost.addAllRunnerPostTags(runnerPostTags); + + return runnerPost; + } + public static RunnerPost create(final Runner runner, final Supporter supporter) { return RunnerPost.builder() .title(new Title("테스트 제목")) @@ -135,10 +164,10 @@ public static RunnerPost create(final Runner runner, final Supporter supporter) .build(); } - public static RunnerPost createWithReviewStatus(final Runner runner, - final Supporter supporter, - final ReviewStatus reviewStatus, - final IsReviewed isReviewed + public static RunnerPost createWithSupporter(final Runner runner, + final Supporter supporter, + final ReviewStatus reviewStatus, + final IsReviewed isReviewed ) { return RunnerPost.builder() .title(new Title("테스트 제목")) diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerPostTagFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerPostTagFixture.java index b244ce1bb..0f1b00316 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerPostTagFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerPostTagFixture.java @@ -1,8 +1,8 @@ package touch.baton.fixture.domain; -import touch.baton.domain.runnerpost.RunnerPost; -import touch.baton.domain.tag.RunnerPostTag; -import touch.baton.domain.tag.Tag; +import touch.baton.domain.runnerpost.command.RunnerPost; +import touch.baton.domain.tag.command.RunnerPostTag; +import touch.baton.domain.tag.command.Tag; public abstract class RunnerPostTagFixture { diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerPostTagsFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerPostTagsFixture.java index 3a62d7096..0505850bd 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerPostTagsFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerPostTagsFixture.java @@ -1,7 +1,7 @@ package touch.baton.fixture.domain; -import touch.baton.domain.tag.RunnerPostTag; -import touch.baton.domain.tag.RunnerPostTags; +import touch.baton.domain.tag.command.RunnerPostTag; +import touch.baton.domain.tag.command.RunnerPostTags; import java.util.List; diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerTechnicalTagsFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerTechnicalTagsFixture.java index 0561a4afc..df1e25c71 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerTechnicalTagsFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerTechnicalTagsFixture.java @@ -1,9 +1,9 @@ package touch.baton.fixture.domain; -import touch.baton.domain.runner.Runner; -import touch.baton.domain.technicaltag.RunnerTechnicalTag; -import touch.baton.domain.technicaltag.RunnerTechnicalTags; -import touch.baton.domain.technicaltag.TechnicalTag; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.technicaltag.command.RunnerTechnicalTag; +import touch.baton.domain.technicaltag.command.RunnerTechnicalTags; +import touch.baton.domain.technicaltag.command.TechnicalTag; import java.util.List; diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterFeedbackFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterFeedbackFixture.java index 9a4bf0192..0365ef389 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterFeedbackFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterFeedbackFixture.java @@ -1,11 +1,11 @@ package touch.baton.fixture.domain; -import touch.baton.domain.feedback.SupporterFeedback; -import touch.baton.domain.feedback.vo.Description; -import touch.baton.domain.feedback.vo.ReviewType; -import touch.baton.domain.runner.Runner; -import touch.baton.domain.runnerpost.RunnerPost; -import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.feedback.command.SupporterFeedback; +import touch.baton.domain.feedback.command.vo.Description; +import touch.baton.domain.feedback.command.vo.ReviewType; +import touch.baton.domain.member.command.Runner; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.runnerpost.command.RunnerPost; import static touch.baton.fixture.vo.DescriptionFixture.description; diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterFixture.java index e5b93fc55..903fd8196 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterFixture.java @@ -1,11 +1,11 @@ package touch.baton.fixture.domain; -import touch.baton.domain.member.Member; -import touch.baton.domain.supporter.Supporter; -import touch.baton.domain.supporter.vo.ReviewCount; -import touch.baton.domain.technicaltag.SupporterTechnicalTag; -import touch.baton.domain.technicaltag.SupporterTechnicalTags; -import touch.baton.domain.technicaltag.TechnicalTag; +import touch.baton.domain.member.command.Member; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.vo.ReviewCount; +import touch.baton.domain.technicaltag.command.SupporterTechnicalTag; +import touch.baton.domain.technicaltag.command.SupporterTechnicalTags; +import touch.baton.domain.technicaltag.command.TechnicalTag; import touch.baton.fixture.vo.ReviewCountFixture; import java.util.ArrayList; diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterRunnerPostFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterRunnerPostFixture.java index c48140d79..7852fc6b4 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterRunnerPostFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterRunnerPostFixture.java @@ -1,9 +1,9 @@ package touch.baton.fixture.domain; -import touch.baton.domain.runnerpost.RunnerPost; -import touch.baton.domain.supporter.Supporter; -import touch.baton.domain.supporter.SupporterRunnerPost; -import touch.baton.domain.supporter.vo.Message; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.member.command.SupporterRunnerPost; +import touch.baton.domain.member.command.vo.Message; +import touch.baton.domain.runnerpost.command.RunnerPost; public abstract class SupporterRunnerPostFixture { diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterTechnicalTagFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterTechnicalTagFixture.java index b67a77f5a..56c222922 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterTechnicalTagFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterTechnicalTagFixture.java @@ -1,8 +1,8 @@ package touch.baton.fixture.domain; -import touch.baton.domain.supporter.Supporter; -import touch.baton.domain.technicaltag.SupporterTechnicalTag; -import touch.baton.domain.technicaltag.TechnicalTag; +import touch.baton.domain.member.command.Supporter; +import touch.baton.domain.technicaltag.command.SupporterTechnicalTag; +import touch.baton.domain.technicaltag.command.TechnicalTag; public abstract class SupporterTechnicalTagFixture { diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterTechnicalTagsFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterTechnicalTagsFixture.java index da7600f6e..380362472 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterTechnicalTagsFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterTechnicalTagsFixture.java @@ -1,7 +1,7 @@ package touch.baton.fixture.domain; -import touch.baton.domain.technicaltag.SupporterTechnicalTag; -import touch.baton.domain.technicaltag.SupporterTechnicalTags; +import touch.baton.domain.technicaltag.command.SupporterTechnicalTag; +import touch.baton.domain.technicaltag.command.SupporterTechnicalTags; import java.util.List; 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 2b659e111..9d27a5789 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 @@ -1,8 +1,8 @@ package touch.baton.fixture.domain; import touch.baton.domain.common.vo.TagName; -import touch.baton.domain.tag.Tag; -import touch.baton.domain.tag.vo.TagReducedName; +import touch.baton.domain.tag.command.Tag; +import touch.baton.domain.tag.command.vo.TagReducedName; import touch.baton.fixture.vo.TagNameFixture; public abstract class TagFixture { diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/TechnicalTagFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/TechnicalTagFixture.java index 3464d9c7d..b1ef78d5e 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/domain/TechnicalTagFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/TechnicalTagFixture.java @@ -1,7 +1,7 @@ package touch.baton.fixture.domain; import touch.baton.domain.common.vo.TagName; -import touch.baton.domain.technicaltag.TechnicalTag; +import touch.baton.domain.technicaltag.command.TechnicalTag; import static touch.baton.fixture.vo.TagNameFixture.tagName; 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 index 10c443506..9a5c7e0a9 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/vo/AuthorizationHeaderFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/AuthorizationHeaderFixture.java @@ -1,6 +1,6 @@ package touch.baton.fixture.vo; -import touch.baton.domain.oauth.AuthorizationHeader; +import touch.baton.domain.oauth.command.AuthorizationHeader; public class AuthorizationHeaderFixture { diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/CompanyFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/CompanyFixture.java index 44532716e..3cd918eb9 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/vo/CompanyFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/CompanyFixture.java @@ -1,6 +1,6 @@ package touch.baton.fixture.vo; -import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.command.vo.Company; public abstract class CompanyFixture { 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 index 768f50574..de1f72670 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/vo/CuriousContentsFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/CuriousContentsFixture.java @@ -1,6 +1,6 @@ package touch.baton.fixture.vo; -import touch.baton.domain.runnerpost.vo.CuriousContents; +import touch.baton.domain.runnerpost.command.vo.CuriousContents; public abstract class CuriousContentsFixture { diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/DeadlineFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/DeadlineFixture.java index a706c4283..34e0f999c 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/vo/DeadlineFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/DeadlineFixture.java @@ -1,6 +1,6 @@ package touch.baton.fixture.vo; -import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.command.vo.Deadline; import java.time.LocalDateTime; 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 9354b8828..f7762441a 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 @@ -1,6 +1,6 @@ package touch.baton.fixture.vo; -import touch.baton.domain.feedback.vo.Description; +import touch.baton.domain.feedback.command.vo.Description; public abstract class 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 index 1d923d90f..98de3a810 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/vo/ExpireDateFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/ExpireDateFixture.java @@ -1,6 +1,6 @@ package touch.baton.fixture.vo; -import touch.baton.domain.oauth.token.ExpireDate; +import touch.baton.domain.oauth.command.token.ExpireDate; import java.time.LocalDateTime; diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/GithubUrlFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/GithubUrlFixture.java index 6eedbdc2d..4f3158ca3 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/vo/GithubUrlFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/GithubUrlFixture.java @@ -1,6 +1,6 @@ package touch.baton.fixture.vo; -import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.command.vo.GithubUrl; public abstract class GithubUrlFixture { diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/ImageUrlFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/ImageUrlFixture.java index 3f3667ffe..e07b7c8f8 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/vo/ImageUrlFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/ImageUrlFixture.java @@ -1,6 +1,6 @@ package touch.baton.fixture.vo; -import touch.baton.domain.member.vo.ImageUrl; +import touch.baton.domain.member.command.vo.ImageUrl; public abstract class ImageUrlFixture { 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 index 468933ee3..fea905937 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/vo/ImplementedContentsFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/ImplementedContentsFixture.java @@ -1,6 +1,6 @@ package touch.baton.fixture.vo; -import touch.baton.domain.runnerpost.vo.ImplementedContents; +import touch.baton.domain.runnerpost.command.vo.ImplementedContents; public abstract class ImplementedContentsFixture { diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/IntroductionFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/IntroductionFixture.java index 596bca95d..446e888fd 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/vo/IntroductionFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/IntroductionFixture.java @@ -1,6 +1,6 @@ package touch.baton.fixture.vo; -import touch.baton.domain.common.vo.Introduction; +import touch.baton.domain.member.command.vo.Introduction; public abstract class IntroductionFixture { diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/MemberNameFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/MemberNameFixture.java index 2160545eb..a86d608af 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/vo/MemberNameFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/MemberNameFixture.java @@ -1,6 +1,6 @@ package touch.baton.fixture.vo; -import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.command.vo.MemberName; public abstract class MemberNameFixture { diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/MessageFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/MessageFixture.java index 7cd2c8c5a..041bf090f 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/vo/MessageFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/MessageFixture.java @@ -1,6 +1,6 @@ package touch.baton.fixture.vo; -import touch.baton.domain.supporter.vo.Message; +import touch.baton.domain.member.command.vo.Message; public abstract class MessageFixture { diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/NotificationMessageFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/NotificationMessageFixture.java new file mode 100644 index 000000000..688ada0a2 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/NotificationMessageFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.notification.command.vo.NotificationMessage; + +public abstract class NotificationMessageFixture { + + private NotificationMessageFixture() { + } + + public static NotificationMessage notificationMessage(final String value) { + return new NotificationMessage(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/NotificationReferencedIdFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/NotificationReferencedIdFixture.java new file mode 100644 index 000000000..56b0d803d --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/NotificationReferencedIdFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.notification.command.vo.NotificationReferencedId; + +public abstract class NotificationReferencedIdFixture { + + private NotificationReferencedIdFixture() { + } + + public static NotificationReferencedId notificationReferencedId(final Long value) { + return new NotificationReferencedId(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/NotificationTitleFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/NotificationTitleFixture.java new file mode 100644 index 000000000..a7d04ec8c --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/NotificationTitleFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.notification.command.vo.NotificationTitle; + +public abstract class NotificationTitleFixture { + + private NotificationTitleFixture() { + } + + public static NotificationTitle notificationTitle(final String value) { + return new NotificationTitle(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/OauthIdFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/OauthIdFixture.java index 519ba6766..331beb1da 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/vo/OauthIdFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/OauthIdFixture.java @@ -1,6 +1,6 @@ package touch.baton.fixture.vo; -import touch.baton.domain.member.vo.OauthId; +import touch.baton.domain.member.command.vo.OauthId; public abstract class OauthIdFixture { 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 index c03fe92fd..162566d52 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/vo/PostscriptContentsFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/PostscriptContentsFixture.java @@ -1,6 +1,6 @@ package touch.baton.fixture.vo; -import touch.baton.domain.runnerpost.vo.PostscriptContents; +import touch.baton.domain.runnerpost.command.vo.PostscriptContents; public abstract class PostscriptContentsFixture { diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/PullRequestUrlFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/PullRequestUrlFixture.java index a28447b46..f49d15436 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/vo/PullRequestUrlFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/PullRequestUrlFixture.java @@ -1,6 +1,6 @@ package touch.baton.fixture.vo; -import touch.baton.domain.runnerpost.vo.PullRequestUrl; +import touch.baton.domain.runnerpost.command.vo.PullRequestUrl; public abstract class PullRequestUrlFixture { diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/ReviewCountFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/ReviewCountFixture.java index 59a70efa8..decac6fad 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/vo/ReviewCountFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/ReviewCountFixture.java @@ -1,6 +1,6 @@ package touch.baton.fixture.vo; -import touch.baton.domain.supporter.vo.ReviewCount; +import touch.baton.domain.member.command.vo.ReviewCount; public abstract class ReviewCountFixture { diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/SocialIdFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/SocialIdFixture.java index 2552b5314..cadfdea60 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/vo/SocialIdFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/SocialIdFixture.java @@ -1,6 +1,6 @@ package touch.baton.fixture.vo; -import touch.baton.domain.member.vo.SocialId; +import touch.baton.domain.member.command.vo.SocialId; public abstract class SocialIdFixture { diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/TitleFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/TitleFixture.java index 1feb686cf..8b5fc8864 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/vo/TitleFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/TitleFixture.java @@ -1,6 +1,6 @@ package touch.baton.fixture.vo; -import touch.baton.domain.common.vo.Title; +import touch.baton.domain.runnerpost.command.vo.Title; public abstract class TitleFixture { 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 index 1ca72494d..bd53649e4 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/vo/TokenFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/TokenFixture.java @@ -1,6 +1,6 @@ package touch.baton.fixture.vo; -import touch.baton.domain.oauth.token.Token; +import touch.baton.domain.oauth.command.token.Token; public abstract class TokenFixture { diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/WatchedCountFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/WatchedCountFixture.java index 54b5551cb..cd482a1a6 100644 --- a/backend/baton/src/test/java/touch/baton/fixture/vo/WatchedCountFixture.java +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/WatchedCountFixture.java @@ -1,6 +1,6 @@ package touch.baton.fixture.vo; -import touch.baton.domain.common.vo.WatchedCount; +import touch.baton.domain.runnerpost.command.vo.WatchedCount; public abstract class WatchedCountFixture { 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 664913a32..13170e70a 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 @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import touch.baton.domain.oauth.exception.OauthRequestException; +import touch.baton.domain.oauth.command.exception.OauthRequestException; import java.util.Map;