From 939d18469af5edf986769fd5d0dfb396fe830e26 Mon Sep 17 00:00:00 2001 From: wyc Date: Sun, 8 Oct 2023 16:26:33 +0900 Subject: [PATCH] =?UTF-8?q?Misison,=20Step,=20PR=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=84=A4=EA=B3=84=20=EB=B0=8F=20PR=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20(#12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 깃허브 인증 및 PR 조회 기능 * refactor: PasswordEncoder 패키지 변경 * chore: env file 제거 * fix: 환경변수 값이 바인딩 되지 않는 문제 * feat: 로그인 기능 * fix: PR List 조회 기능 UnAuthorized 문제 * fix: Auth ArgumentResolver 등록 안되는 문제 * refactor: 레포별 모든 PR 조회 기능 컨트롤러 변경 * refactor: 변수 final 키워드 추가 * refactor: jwt payload memberId로 변경 * fix: Querydsl Qclass 빌드 문제 * feat: 현재 로그인한 유저의 미션에 대한 모든 PullRequest 조회 * feat: 레파지토리 이름을 입력 받으면 해당 레포지토리에 있는 로그인한 유저의 PR들을 동기화한다. * refactor: api path 소유격에서 소유대명사로 수정 * refactor: 동기화시 이미 존재하는 PR이라면 최신 정보로 수정만, 존재하지 않는다면 PR 생성 * refactor: 메서드명 수정 * refactor: GithubClientApi 상수 클래스로 관리 * refactor: Client 통신용 DTO 분리 * refactor: 문자열 포맷팅은 Constants 클래스에서 담당한다. * refactor: PR 동기화시 존재하지 않는 PR만 저장한다. * docs: 리팩터링 TODO * refactor: 토큰 발급 메서드명 변경 * chore: 메서드 개행 * chore: 필드 개행 * chore: 이메일 레디스 해쉬 삭제 * refactor: DTO 이름 통일 * refactor: JsonNaming 사용 * chore: final 키워드 추가 * chore: 리팩터링 TODO * chore: 메서드 개행 * chore: 사용하지 않는 import 제거 * fix: QClass 필드명 수정 * refactor: 토큰 조회시 token type이랑 같이 조회 * refactor: Type Enum 분리 * refactor: passwordEncoder 파라미터 이름 변경 --- .idea/compiler.xml | 12 ++ .idea/modules.xml | 8 - .idea/modules/techhub.main.iml | 8 - .idea/vcs.xml | 1 + build.gradle | 167 ++++++++++-------- docker/docker-compose.local.yml | 12 +- .../auth/application/AuthQueryService.java | 4 +- .../techhub/auth/application/AuthService.java | 25 ++- .../auth/application/client/GithubClient.java | 19 ++ .../client/GithubClientProperties.java | 11 ++ .../client/GithubClientQueryService.java | 51 ++++++ .../client/GithubRestTemplateClient.java | 91 ++++++++++ .../request/GithubTokenRefreshRequest.java | 16 ++ .../dto/request/GithubTokenRequest.java | 13 ++ .../dto/response/GithubPrInfoResponse.java | 37 ++++ .../response/OAuthGithubUsernameResponse.java | 9 + .../dto/response/OAuthTokensResponse.java | 37 ++++ .../techhub/auth/domain/AccessToken.java | 32 ++++ .../techhub/auth/domain/RefreshToken.java | 31 ++++ .../repository/AccessTokenRepository.java | 13 ++ .../repository/RefreshTokenRepository.java | 13 ++ .../techhub/auth/domain/type/Type.java | 6 + .../auth/dto/request/LoginRequest.java | 8 + .../auth/dto/{ => request}/SignUpRequest.java | 2 +- .../auth/dto/response/TokenResponse.java | 8 + .../GithubRefreshTokenNotFoundException.java | 14 ++ .../auth/presentation/AuthController.java | 31 +++- .../techhub/auth/util/GithubApiConstants.java | 46 +++++ .../techhub/common/auth/annotation/Auth.java | 12 ++ .../auth}/encode/BCryptPasswordEncoder.java | 2 +- .../common/auth/jwt/BearerTokenExtractor.java | 28 +++ .../common/auth/jwt/JwtProperties.java | 14 ++ .../techhub/common/auth/jwt/JwtProvider.java | 67 +++++++ .../jwt/exception/TokenInvalidException.java | 14 ++ .../auth/resolver/AuthArgumentResolver.java | 42 +++++ .../common/auth/resolver/AuthProperties.java | 6 + .../common/config/EmbeddedRedisConfig.java | 3 +- .../common/config/PropertiesConfig.java | 15 ++ .../techhub/common/config/WebConfig.java | 38 ++++ .../techhub/mail/application/MailService.java | 2 +- .../AuthorityCodeNotMatchException.java | 1 - .../mail/presentation/MailController.java | 2 +- .../techhub/member/domain/Member.java | 14 +- .../domain/repository/MemberRepository.java | 16 +- ...java => MemberAlreadyExistsException.java} | 4 +- .../exception/MemberNotFoundException.java | 18 ++ .../exception/PasswordNotMatchException.java | 14 ++ .../techhub/mission/domain/Mission.java | 34 ++++ .../techhub/mission/domain/Step.java | 30 ++++ .../exception/StepNotFoundException.java | 17 ++ .../domain/repository/StepRepository.java | 17 ++ .../mission/domain/type/StepStatus.java | 8 + .../application/PullRequestQueryService.java | 16 +- .../pr/application/PullRequestService.java | 62 +++++++ .../techhub/pr/domain/PullRequest.java | 12 +- .../PullRequestQueryRepository.java | 11 -- .../repository/PullRequestRepository.java | 4 +- .../domain/type/{Status.java => State.java} | 8 +- .../dto/response/GetPullRequestResponse.java | 28 --- .../pr/dto/response/PullRequestResponse.java | 20 +++ .../infra/dto/PullRequestQueryResponse.java | 16 ++ .../persist/PullRequestQueryRepository.java | 11 ++ .../PullRequestQueryRepositoryImpl.java | 41 +++++ .../presentation/PullRequestController.java | 43 ++++- src/main/resources/application-dev.yml | 1 - src/main/resources/application-local.yml | 2 +- src/main/resources/application-prod.yml | 1 - src/main/resources/application.yml | 20 ++- src/main/resources/database-env.properties | 3 + src/main/resources/jwt-env.properties | 5 + .../application/AuthQueryServiceTest.java | 4 +- .../auth/application/AuthServiceTest.java | 2 +- .../encode/BCryptPasswordEncoderTest.java | 1 + .../presentation/MailControllerMockTest.java | 110 ++++++------ .../member/domain/AuthorityCodeTest.java | 2 +- .../techhub/member/domain/MemberTest.java | 2 +- .../techhub/member/fixture/MemberFixture.java | 2 +- .../PullRequestQueryServiceTest.java | 42 ----- .../pr/domain/fixture/PullRequestFixture.java | 17 -- .../repository/PullRequestRepositoryTest.java | 39 ---- .../PullRequestControllerSteps.java | 28 --- .../PullRequestControllerTest.java | 52 ------ 82 files changed, 1315 insertions(+), 433 deletions(-) delete mode 100644 .idea/modules.xml delete mode 100644 .idea/modules/techhub.main.iml create mode 100644 src/main/java/com/integrated/techhub/auth/application/client/GithubClient.java create mode 100644 src/main/java/com/integrated/techhub/auth/application/client/GithubClientProperties.java create mode 100644 src/main/java/com/integrated/techhub/auth/application/client/GithubClientQueryService.java create mode 100644 src/main/java/com/integrated/techhub/auth/application/client/GithubRestTemplateClient.java create mode 100644 src/main/java/com/integrated/techhub/auth/application/client/dto/request/GithubTokenRefreshRequest.java create mode 100644 src/main/java/com/integrated/techhub/auth/application/client/dto/request/GithubTokenRequest.java create mode 100644 src/main/java/com/integrated/techhub/auth/application/client/dto/response/GithubPrInfoResponse.java create mode 100644 src/main/java/com/integrated/techhub/auth/application/client/dto/response/OAuthGithubUsernameResponse.java create mode 100644 src/main/java/com/integrated/techhub/auth/application/client/dto/response/OAuthTokensResponse.java create mode 100644 src/main/java/com/integrated/techhub/auth/domain/AccessToken.java create mode 100644 src/main/java/com/integrated/techhub/auth/domain/RefreshToken.java create mode 100644 src/main/java/com/integrated/techhub/auth/domain/repository/AccessTokenRepository.java create mode 100644 src/main/java/com/integrated/techhub/auth/domain/repository/RefreshTokenRepository.java create mode 100644 src/main/java/com/integrated/techhub/auth/domain/type/Type.java create mode 100644 src/main/java/com/integrated/techhub/auth/dto/request/LoginRequest.java rename src/main/java/com/integrated/techhub/auth/dto/{ => request}/SignUpRequest.java (97%) create mode 100644 src/main/java/com/integrated/techhub/auth/dto/response/TokenResponse.java create mode 100644 src/main/java/com/integrated/techhub/auth/exception/GithubRefreshTokenNotFoundException.java create mode 100644 src/main/java/com/integrated/techhub/auth/util/GithubApiConstants.java create mode 100644 src/main/java/com/integrated/techhub/common/auth/annotation/Auth.java rename src/main/java/com/integrated/techhub/{auth/infra => common/auth}/encode/BCryptPasswordEncoder.java (90%) create mode 100644 src/main/java/com/integrated/techhub/common/auth/jwt/BearerTokenExtractor.java create mode 100644 src/main/java/com/integrated/techhub/common/auth/jwt/JwtProperties.java create mode 100644 src/main/java/com/integrated/techhub/common/auth/jwt/JwtProvider.java create mode 100644 src/main/java/com/integrated/techhub/common/auth/jwt/exception/TokenInvalidException.java create mode 100644 src/main/java/com/integrated/techhub/common/auth/resolver/AuthArgumentResolver.java create mode 100644 src/main/java/com/integrated/techhub/common/auth/resolver/AuthProperties.java create mode 100644 src/main/java/com/integrated/techhub/common/config/PropertiesConfig.java create mode 100644 src/main/java/com/integrated/techhub/common/config/WebConfig.java rename src/main/java/com/integrated/techhub/member/exception/{MemberExistsException.java => MemberAlreadyExistsException.java} (74%) create mode 100644 src/main/java/com/integrated/techhub/member/exception/MemberNotFoundException.java create mode 100644 src/main/java/com/integrated/techhub/member/exception/PasswordNotMatchException.java create mode 100644 src/main/java/com/integrated/techhub/mission/domain/Mission.java create mode 100644 src/main/java/com/integrated/techhub/mission/domain/Step.java create mode 100644 src/main/java/com/integrated/techhub/mission/domain/exception/StepNotFoundException.java create mode 100644 src/main/java/com/integrated/techhub/mission/domain/repository/StepRepository.java create mode 100644 src/main/java/com/integrated/techhub/mission/domain/type/StepStatus.java create mode 100644 src/main/java/com/integrated/techhub/pr/application/PullRequestService.java delete mode 100644 src/main/java/com/integrated/techhub/pr/domain/repository/PullRequestQueryRepository.java rename src/main/java/com/integrated/techhub/pr/domain/type/{Status.java => State.java} (50%) delete mode 100644 src/main/java/com/integrated/techhub/pr/dto/response/GetPullRequestResponse.java create mode 100644 src/main/java/com/integrated/techhub/pr/dto/response/PullRequestResponse.java create mode 100644 src/main/java/com/integrated/techhub/pr/infra/dto/PullRequestQueryResponse.java create mode 100644 src/main/java/com/integrated/techhub/pr/infra/persist/PullRequestQueryRepository.java create mode 100644 src/main/java/com/integrated/techhub/pr/infra/persist/PullRequestQueryRepositoryImpl.java create mode 100644 src/main/resources/database-env.properties create mode 100644 src/main/resources/jwt-env.properties delete mode 100644 src/test/java/com/integrated/techhub/pr/application/PullRequestQueryServiceTest.java delete mode 100644 src/test/java/com/integrated/techhub/pr/domain/fixture/PullRequestFixture.java delete mode 100644 src/test/java/com/integrated/techhub/pr/domain/repository/PullRequestRepositoryTest.java delete mode 100644 src/test/java/com/integrated/techhub/pr/presentation/PullRequestControllerSteps.java delete mode 100644 src/test/java/com/integrated/techhub/pr/presentation/PullRequestControllerTest.java diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 03555ce..f5e3843 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -2,10 +2,22 @@ + + + + + + + + + + + + diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 7ab477a..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/modules/techhub.main.iml b/.idea/modules/techhub.main.iml deleted file mode 100644 index ec81b53..0000000 --- a/.idea/modules/techhub.main.iml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1dd..8306744 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 7be20d3..db78a33 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'jacoco' +// id 'jacoco' id 'org.springframework.boot' version '3.1.3' id 'io.spring.dependency-management' version '1.1.3' } @@ -12,9 +12,9 @@ java { sourceCompatibility = '17' } -jacoco { - toolVersion = "0.8.8" -} +//jacoco { +// toolVersion = "0.8.8" +//} configurations { compileOnly { @@ -30,6 +30,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" implementation 'org.mindrot:jbcrypt:0.4' //redis @@ -41,6 +42,11 @@ dependencies { runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + // Mail implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' @@ -62,6 +68,7 @@ tasks.named('test') { } def generated = 'build/generated' +def application = 'src/main/generated' tasks.withType(JavaCompile) { options.getGeneratedSourceOutputDirectory().set(file(generated)) @@ -71,84 +78,88 @@ sourceSets { main.java.srcDirs += [generated] } -clean { +task customClean(type: Delete) { delete file(generated) + delete file(application) } -jacocoTestReport { - reports { - html.required = true - - html.destination file("${buildDir}/jacoco/index.html") - } - - afterEvaluate { - classDirectories.setFrom( - files(classDirectories.files.collect { - fileTree(dir: it, excludes: [ - '**/*Application*', - '**/*Exception*', - '**/*Response*', - '**/*Request*', - '**/BaseTimeEntity', - '**/*Dto*', - '**/S3*', - '**/*Interceptor*', - '**/*ArgumentResolver*', - '**/*ExceptionHandler*', - '**/LoggingUtils', - '**/*Url*', - '**/*AdminController*', - '**/*Config*', - '**/*Wrapper*' - ]) - }) - ) - } - - finalizedBy 'jacocoTestCoverageVerification' -} - -jacocoTestCoverageVerification { - def Qdomains = [] - - for (qPattern in '*.QA'..'*.QZ') { // qPattern = '*.QA', '*.QB', ... '*.QZ' - Qdomains.add(qPattern + '*') - } - - violationRules { - rule { - enabled = true - element = "CLASS" - - limit { - counter = 'LINE' - value = 'COVEREDRATIO' - minimum = 0.7 - } - - excludes = [ - '*.*Application', - '*.*Exception*', - '*.*Dto', - '*.S3*', - '*.*Response', - '*.*Request', - '*.BaseTimeEntity', - '*.*Interceptor', - '*.*ArgumentResolver', - '*.*ExceptionHandler', - '*.LoggingUtils', - '*.*Url', - '*.*AdminController', - '*.*Config', - '*.*Wrapper' - ] + Qdomains - } - } -} +clean.dependsOn customClean + + +//jacocoTestReport { +// reports { +// html.required = true +// +// html.destination file("${buildDir}/jacoco/index.html") +// } +// +// afterEvaluate { +// classDirectories.setFrom( +// files(classDirectories.files.collect { +// fileTree(dir: it, excludes: [ +// '**/*Application*', +// '**/*Exception*', +// '**/*Response*', +// '**/*Request*', +// '**/BaseTimeEntity', +// '**/*Dto*', +// '**/S3*', +// '**/*Interceptor*', +// '**/*ArgumentResolver*', +// '**/*ExceptionHandler*', +// '**/LoggingUtils', +// '**/*Url*', +// '**/*AdminController*', +// '**/*Config*', +// '**/*Wrapper*' +// ]) +// }) +// ) +// } +// +// finalizedBy 'jacocoTestCoverageVerification' +//} + +//jacocoTestCoverageVerification { +// def Qdomains = [] +// +// for (qPattern in '*.QA'..'*.QZ') { // qPattern = '*.QA', '*.QB', ... '*.QZ' +// Qdomains.add(qPattern + '*') +// } +// +// violationRules { +// rule { +// enabled = true +// element = "CLASS" +// +// limit { +// counter = 'LINE' +// value = 'COVEREDRATIO' +// minimum = 0.7 +// } +// +// excludes = [ +// '*.*Application', +// '*.*Exception*', +// '*.*Dto', +// '*.S3*', +// '*.*Response', +// '*.*Request', +// '*.BaseTimeEntity', +// '*.*Interceptor', +// '*.*ArgumentResolver', +// '*.*ExceptionHandler', +// '*.LoggingUtils', +// '*.*Url', +// '*.*AdminController', +// '*.*Config', +// '*.*Wrapper' +// ] + Qdomains +// } +// } +//} test { useJUnitPlatform() - finalizedBy 'jacocoTestReport' +// finalizedBy 'jacocoTestReport' } \ No newline at end of file diff --git a/docker/docker-compose.local.yml b/docker/docker-compose.local.yml index 213f36a..ca5d02f 100644 --- a/docker/docker-compose.local.yml +++ b/docker/docker-compose.local.yml @@ -2,7 +2,7 @@ version: '3' services: mysql: - container_name: techhub + container_name: mysql image: mysql/mysql-server environment: MYSQL_DATABASE: techhub @@ -16,3 +16,13 @@ services: - "--character-set-server=utf8mb4" - "--collation-server=utf8mb4_unicode_ci" + redis: + container_name: redis + image: redis:alpine + command: redis-server --port 6379 + hostname: localhost + labels: + - "name=redis" + - "mode=standalone" + ports: + - 16379:6379 diff --git a/src/main/java/com/integrated/techhub/auth/application/AuthQueryService.java b/src/main/java/com/integrated/techhub/auth/application/AuthQueryService.java index 33fc2f8..39a3761 100644 --- a/src/main/java/com/integrated/techhub/auth/application/AuthQueryService.java +++ b/src/main/java/com/integrated/techhub/auth/application/AuthQueryService.java @@ -1,7 +1,7 @@ package com.integrated.techhub.auth.application; import com.integrated.techhub.member.domain.repository.MemberRepository; -import com.integrated.techhub.member.exception.MemberExistsException; +import com.integrated.techhub.member.exception.MemberAlreadyExistsException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -13,7 +13,7 @@ public class AuthQueryService { public void validateExistedMember(final String email) { if (memberRepository.existsByEmail(email)) { - throw new MemberExistsException(); + throw new MemberAlreadyExistsException(); } } diff --git a/src/main/java/com/integrated/techhub/auth/application/AuthService.java b/src/main/java/com/integrated/techhub/auth/application/AuthService.java index cfbba16..9047f70 100644 --- a/src/main/java/com/integrated/techhub/auth/application/AuthService.java +++ b/src/main/java/com/integrated/techhub/auth/application/AuthService.java @@ -1,7 +1,13 @@ package com.integrated.techhub.auth.application; +import com.integrated.techhub.auth.domain.AccessToken; import com.integrated.techhub.auth.domain.PasswordEncoder; -import com.integrated.techhub.auth.dto.SignUpRequest; +import com.integrated.techhub.auth.domain.RefreshToken; +import com.integrated.techhub.auth.domain.repository.AccessTokenRepository; +import com.integrated.techhub.auth.domain.repository.RefreshTokenRepository; +import com.integrated.techhub.auth.dto.request.SignUpRequest; +import com.integrated.techhub.auth.dto.response.TokenResponse; +import com.integrated.techhub.common.auth.jwt.JwtProvider; import com.integrated.techhub.member.domain.Member; import com.integrated.techhub.member.domain.repository.MemberRepository; import lombok.RequiredArgsConstructor; @@ -13,15 +19,30 @@ @RequiredArgsConstructor public class AuthService { + private final JwtProvider jwtProvider; private final MemberRepository memberRepository; private final AuthQueryService authQueryService; private final PasswordEncoder passwordEncoder; + private final AccessTokenRepository accessTokenRepository; + private final RefreshTokenRepository refreshTokenRepository; public Long registerMember(final SignUpRequest request) { authQueryService.validateExistedMember(request.email()); - Member member = request.toEntity(); + final Member member = request.toEntity(); member.encodePassword(passwordEncoder); return memberRepository.save(member).getId(); } + public TokenResponse getTokens(final String email, final String password) { + final Member member = memberRepository.getByEmail(email); + member.validateMatchPassword(passwordEncoder, password); + final String accessToken = jwtProvider.generateAccessToken(member.getId()); + final String refreshToken = jwtProvider.generateRefreshToken(member.getId()).getToken(); + return new TokenResponse(accessToken, refreshToken); + } + + public void saveGithubTokens(final AccessToken accessToken, final RefreshToken refreshToken) { + accessTokenRepository.save(accessToken); + refreshTokenRepository.save(refreshToken); + } } diff --git a/src/main/java/com/integrated/techhub/auth/application/client/GithubClient.java b/src/main/java/com/integrated/techhub/auth/application/client/GithubClient.java new file mode 100644 index 0000000..d8c2c5b --- /dev/null +++ b/src/main/java/com/integrated/techhub/auth/application/client/GithubClient.java @@ -0,0 +1,19 @@ +package com.integrated.techhub.auth.application.client; + +import com.integrated.techhub.auth.application.client.dto.response.GithubPrInfoResponse; +import com.integrated.techhub.auth.application.client.dto.response.OAuthGithubUsernameResponse; +import com.integrated.techhub.auth.application.client.dto.response.OAuthTokensResponse; + +import java.util.List; + +public interface GithubClient { + + OAuthTokensResponse getGithubTokens(final String code); + + OAuthTokensResponse getNewAccessToken(final String refreshToken); + + OAuthGithubUsernameResponse getGithubUsername(final String accessToken); + + List getPrsByRepoName(final String accessToken, final String repo); + +} diff --git a/src/main/java/com/integrated/techhub/auth/application/client/GithubClientProperties.java b/src/main/java/com/integrated/techhub/auth/application/client/GithubClientProperties.java new file mode 100644 index 0000000..37a4984 --- /dev/null +++ b/src/main/java/com/integrated/techhub/auth/application/client/GithubClientProperties.java @@ -0,0 +1,11 @@ +package com.integrated.techhub.auth.application.client; + + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "spring.security.oauth2.client.registration.github") +public record GithubClientProperties ( + String clientId, + String clientSecret +) { +} diff --git a/src/main/java/com/integrated/techhub/auth/application/client/GithubClientQueryService.java b/src/main/java/com/integrated/techhub/auth/application/client/GithubClientQueryService.java new file mode 100644 index 0000000..8722d33 --- /dev/null +++ b/src/main/java/com/integrated/techhub/auth/application/client/GithubClientQueryService.java @@ -0,0 +1,51 @@ +package com.integrated.techhub.auth.application.client; + +import com.integrated.techhub.auth.application.client.dto.response.GithubPrInfoResponse; +import com.integrated.techhub.auth.domain.AccessToken; +import com.integrated.techhub.auth.domain.RefreshToken; +import com.integrated.techhub.auth.domain.repository.AccessTokenRepository; +import com.integrated.techhub.auth.domain.repository.RefreshTokenRepository; +import com.integrated.techhub.auth.domain.type.Type; +import com.integrated.techhub.auth.exception.GithubRefreshTokenNotFoundException; +import com.integrated.techhub.member.domain.Member; +import com.integrated.techhub.member.domain.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +import static com.integrated.techhub.auth.domain.type.Type.GITHUB; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class GithubClientQueryService { + + private final MemberRepository memberRepository; + private final GithubRestTemplateClient githubRestTemplateClient; + private final AccessTokenRepository accessTokenRepository; + private final RefreshTokenRepository refreshTokenRepository; + + public List getPrsByRepoName(final Long memberId, final String repo) { + final Member member = memberRepository.getById(memberId); + final Optional accessTokenOptional = accessTokenRepository.findByMemberIdAndType(member.getId(), GITHUB); + final Optional refreshTokenOptional = refreshTokenRepository.findByMemberIdAndType(member.getId(), GITHUB); + + // 액세스 토큰이 만료되지 않았다면 액세스 토큰으로 요청 + if (accessTokenOptional.isPresent()) { + final String accessToken = accessTokenOptional.get().getToken(); + return githubRestTemplateClient.getPrsByRepoName(accessToken, repo); + } + // 액세스 토큰이 만료되었다면 리프레시 토큰으로 액세스 토큰 재발급 후 재발급 받은 액세스 토큰으로 요청 + if (refreshTokenOptional.isPresent()) { + final String refreshToken = refreshTokenOptional.get().getToken(); + final String accessToken = githubRestTemplateClient.getNewAccessToken(refreshToken).accessToken(); + return githubRestTemplateClient.getPrsByRepoName(accessToken, repo); + } + // 리프레시 토큰까지 만료되었다면 유저에게 재로그인 요청 + throw new GithubRefreshTokenNotFoundException(); + } + +} diff --git a/src/main/java/com/integrated/techhub/auth/application/client/GithubRestTemplateClient.java b/src/main/java/com/integrated/techhub/auth/application/client/GithubRestTemplateClient.java new file mode 100644 index 0000000..9e3dd5e --- /dev/null +++ b/src/main/java/com/integrated/techhub/auth/application/client/GithubRestTemplateClient.java @@ -0,0 +1,91 @@ +package com.integrated.techhub.auth.application.client; + +import com.integrated.techhub.auth.application.client.dto.request.GithubTokenRefreshRequest; +import com.integrated.techhub.auth.application.client.dto.request.GithubTokenRequest; +import com.integrated.techhub.auth.application.client.dto.response.GithubPrInfoResponse; +import com.integrated.techhub.auth.application.client.dto.response.OAuthGithubUsernameResponse; +import com.integrated.techhub.auth.application.client.dto.response.OAuthTokensResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.List; + +import static com.integrated.techhub.auth.util.GithubApiConstants.*; +import static org.springframework.http.HttpMethod.GET; + +@Component +@RequiredArgsConstructor +public class GithubRestTemplateClient implements GithubClient { + + private static final RestTemplate restTemplate = new RestTemplate(); + + private final GithubClientProperties githubClientProperties; + + @Override + public OAuthTokensResponse getGithubTokens(final String code) { + final String clientId = githubClientProperties.clientId(); + final String clientSecret = githubClientProperties.clientSecret(); + return restTemplate.postForObject( + getGithubTokenUrl(), + new GithubTokenRequest(clientId, clientSecret, code), + OAuthTokensResponse.class + ); + } + + @Override + public OAuthTokensResponse getNewAccessToken(final String refreshToken) { + final String clientId = githubClientProperties.clientId(); + final String clientSecret = githubClientProperties.clientSecret(); + return restTemplate.postForObject( + getNewAccessTokenUrl(clientId, clientSecret, refreshToken), + new GithubTokenRefreshRequest(clientId, clientSecret, refreshToken), + OAuthTokensResponse.class + ); + } + + @Override + @Deprecated + public OAuthGithubUsernameResponse getGithubUsername(final String accessToken) { + final HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + final HttpEntity request = new HttpEntity<>(headers); + return restTemplate.exchange( + getMemberInfoUrl(), + GET, + request, + OAuthGithubUsernameResponse.class + ).getBody(); + } + + // TODO: Require Refactor + @Override + public List getPrsByRepoName(final String accessToken, final String repo) { + final List responses = new ArrayList<>(); + int page = 1; + while (true) { + final List prs = fetchPrs(accessToken, getListPullRequestUrl(repo, page)); + if (prs.isEmpty()) break; + responses.addAll(prs); + page++; + } + return responses; + } + + private List fetchPrs(final String accessToken, final String url) { + final HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + final HttpEntity request = new HttpEntity<>(headers); + return restTemplate.exchange( + url, + GET, + request, + new ParameterizedTypeReference>() {} + ).getBody(); + } + +} diff --git a/src/main/java/com/integrated/techhub/auth/application/client/dto/request/GithubTokenRefreshRequest.java b/src/main/java/com/integrated/techhub/auth/application/client/dto/request/GithubTokenRefreshRequest.java new file mode 100644 index 0000000..8424bac --- /dev/null +++ b/src/main/java/com/integrated/techhub/auth/application/client/dto/request/GithubTokenRefreshRequest.java @@ -0,0 +1,16 @@ +package com.integrated.techhub.auth.application.client.dto.request; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record GithubTokenRefreshRequest( + @JsonProperty("client_id") String clientId, + @JsonProperty("client_secret") String clientSecret, + String grantType, + String refreshToken +) { + + public GithubTokenRefreshRequest(final String clientId, final String clientSecret, final String refreshToken) { + this(clientId, clientSecret, "refresh_token", refreshToken); + } + +} diff --git a/src/main/java/com/integrated/techhub/auth/application/client/dto/request/GithubTokenRequest.java b/src/main/java/com/integrated/techhub/auth/application/client/dto/request/GithubTokenRequest.java new file mode 100644 index 0000000..99ef1ae --- /dev/null +++ b/src/main/java/com/integrated/techhub/auth/application/client/dto/request/GithubTokenRequest.java @@ -0,0 +1,13 @@ +package com.integrated.techhub.auth.application.client.dto.request; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record GithubTokenRequest( + @JsonProperty("client_id") + String clientId, + @JsonProperty("client_secret") + String clientSecret, + String code +) { + +} diff --git a/src/main/java/com/integrated/techhub/auth/application/client/dto/response/GithubPrInfoResponse.java b/src/main/java/com/integrated/techhub/auth/application/client/dto/response/GithubPrInfoResponse.java new file mode 100644 index 0000000..66e1b7e --- /dev/null +++ b/src/main/java/com/integrated/techhub/auth/application/client/dto/response/GithubPrInfoResponse.java @@ -0,0 +1,37 @@ +package com.integrated.techhub.auth.application.client.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.integrated.techhub.pr.domain.PullRequest; +import com.integrated.techhub.pr.domain.type.State; + +import java.util.Date; + +@JsonNaming(SnakeCaseStrategy.class) +public record GithubPrInfoResponse( + @JsonProperty("html_url") String url, + String title, + Integer number, + String state, + Date createdAt, + Date mergedAt, + UserDto user +) { + + @JsonNaming(SnakeCaseStrategy.class) + public record UserDto( + String login + ) { + } + + public PullRequest toPullRequest(final Long memberId, final Long stepId) { + return PullRequest.builder() + .title(title) + .memberId(memberId) + .stepId(stepId) + .state(State.valueOf(state.toUpperCase())) + .build(); + } + +} diff --git a/src/main/java/com/integrated/techhub/auth/application/client/dto/response/OAuthGithubUsernameResponse.java b/src/main/java/com/integrated/techhub/auth/application/client/dto/response/OAuthGithubUsernameResponse.java new file mode 100644 index 0000000..c5d890d --- /dev/null +++ b/src/main/java/com/integrated/techhub/auth/application/client/dto/response/OAuthGithubUsernameResponse.java @@ -0,0 +1,9 @@ +package com.integrated.techhub.auth.application.client.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record OAuthGithubUsernameResponse( + @JsonProperty("login") String username +) { + +} diff --git a/src/main/java/com/integrated/techhub/auth/application/client/dto/response/OAuthTokensResponse.java b/src/main/java/com/integrated/techhub/auth/application/client/dto/response/OAuthTokensResponse.java new file mode 100644 index 0000000..1932ab4 --- /dev/null +++ b/src/main/java/com/integrated/techhub/auth/application/client/dto/response/OAuthTokensResponse.java @@ -0,0 +1,37 @@ +package com.integrated.techhub.auth.application.client.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.integrated.techhub.auth.domain.AccessToken; +import com.integrated.techhub.auth.domain.RefreshToken; +import com.integrated.techhub.auth.domain.type.Type; + +import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import static com.integrated.techhub.auth.domain.type.Type.GITHUB; + +@JsonNaming(SnakeCaseStrategy.class) +public record OAuthTokensResponse ( + String accessToken, + String refreshToken, + @JsonProperty("expires_in") Long accessTokenExp, + @JsonProperty("refresh_token_expires_in") Long refreshTokenExp +) { + public AccessToken toAccessToken(final Long memberId) { + return AccessToken.builder() + .memberId(memberId) + .token(accessToken) + .ttl(accessTokenExp) + .type(GITHUB) + .build(); + } + + public RefreshToken toRefreshToken(final Long memberId) { + return RefreshToken.builder() + .memberId(memberId) + .token(refreshToken) + .ttl(refreshTokenExp) + .type(GITHUB) + .build(); + } + +} diff --git a/src/main/java/com/integrated/techhub/auth/domain/AccessToken.java b/src/main/java/com/integrated/techhub/auth/domain/AccessToken.java new file mode 100644 index 0000000..a88a7d6 --- /dev/null +++ b/src/main/java/com/integrated/techhub/auth/domain/AccessToken.java @@ -0,0 +1,32 @@ +package com.integrated.techhub.auth.domain; + +import com.integrated.techhub.auth.domain.type.Type; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.TimeToLive; +import org.springframework.data.redis.core.index.Indexed; + +@Getter +@Builder +@AllArgsConstructor +@RedisHash(value = "access_token") +public class AccessToken { + + @Id + private Long id; + + @Indexed + private Long memberId; + + @Indexed + private Type type; + + private String token; + + @TimeToLive + private Long ttl; + +} diff --git a/src/main/java/com/integrated/techhub/auth/domain/RefreshToken.java b/src/main/java/com/integrated/techhub/auth/domain/RefreshToken.java new file mode 100644 index 0000000..44d0593 --- /dev/null +++ b/src/main/java/com/integrated/techhub/auth/domain/RefreshToken.java @@ -0,0 +1,31 @@ +package com.integrated.techhub.auth.domain; + +import com.integrated.techhub.auth.domain.type.Type; +import lombok.*; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.TimeToLive; +import org.springframework.data.redis.core.index.Indexed; + +@Getter +@Builder +@RedisHash(value = "refresh_token") +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RefreshToken { + + @Id + private Long id; + + @Indexed + private Long memberId; + + @Indexed + private Type type; + + private String token; + + @TimeToLive + private Long ttl; + +} diff --git a/src/main/java/com/integrated/techhub/auth/domain/repository/AccessTokenRepository.java b/src/main/java/com/integrated/techhub/auth/domain/repository/AccessTokenRepository.java new file mode 100644 index 0000000..02ac135 --- /dev/null +++ b/src/main/java/com/integrated/techhub/auth/domain/repository/AccessTokenRepository.java @@ -0,0 +1,13 @@ +package com.integrated.techhub.auth.domain.repository; + +import com.integrated.techhub.auth.domain.AccessToken; +import com.integrated.techhub.auth.domain.type.Type; +import org.springframework.data.repository.CrudRepository; + +import java.util.Optional; + +public interface AccessTokenRepository extends CrudRepository { + + Optional findByMemberIdAndType(final Long memberId, final Type type); + +} diff --git a/src/main/java/com/integrated/techhub/auth/domain/repository/RefreshTokenRepository.java b/src/main/java/com/integrated/techhub/auth/domain/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..24007e9 --- /dev/null +++ b/src/main/java/com/integrated/techhub/auth/domain/repository/RefreshTokenRepository.java @@ -0,0 +1,13 @@ +package com.integrated.techhub.auth.domain.repository; + +import com.integrated.techhub.auth.domain.RefreshToken; +import com.integrated.techhub.auth.domain.type.Type; +import org.springframework.data.repository.CrudRepository; + +import java.util.Optional; + +public interface RefreshTokenRepository extends CrudRepository { + + Optional findByMemberIdAndType(final Long memberId, final Type type); + +} diff --git a/src/main/java/com/integrated/techhub/auth/domain/type/Type.java b/src/main/java/com/integrated/techhub/auth/domain/type/Type.java new file mode 100644 index 0000000..cf94daa --- /dev/null +++ b/src/main/java/com/integrated/techhub/auth/domain/type/Type.java @@ -0,0 +1,6 @@ +package com.integrated.techhub.auth.domain.type; + +public enum Type { + GITHUB, + TECHHUB +} diff --git a/src/main/java/com/integrated/techhub/auth/dto/request/LoginRequest.java b/src/main/java/com/integrated/techhub/auth/dto/request/LoginRequest.java new file mode 100644 index 0000000..ee54839 --- /dev/null +++ b/src/main/java/com/integrated/techhub/auth/dto/request/LoginRequest.java @@ -0,0 +1,8 @@ +package com.integrated.techhub.auth.dto.request; + +public record LoginRequest( + String email, + String password +) { + +} diff --git a/src/main/java/com/integrated/techhub/auth/dto/SignUpRequest.java b/src/main/java/com/integrated/techhub/auth/dto/request/SignUpRequest.java similarity index 97% rename from src/main/java/com/integrated/techhub/auth/dto/SignUpRequest.java rename to src/main/java/com/integrated/techhub/auth/dto/request/SignUpRequest.java index 4db4aad..a14b76e 100644 --- a/src/main/java/com/integrated/techhub/auth/dto/SignUpRequest.java +++ b/src/main/java/com/integrated/techhub/auth/dto/request/SignUpRequest.java @@ -1,4 +1,4 @@ -package com.integrated.techhub.auth.dto; +package com.integrated.techhub.auth.dto.request; import com.integrated.techhub.member.domain.Member; import com.integrated.techhub.member.domain.Position; diff --git a/src/main/java/com/integrated/techhub/auth/dto/response/TokenResponse.java b/src/main/java/com/integrated/techhub/auth/dto/response/TokenResponse.java new file mode 100644 index 0000000..270a606 --- /dev/null +++ b/src/main/java/com/integrated/techhub/auth/dto/response/TokenResponse.java @@ -0,0 +1,8 @@ +package com.integrated.techhub.auth.dto.response; + +public record TokenResponse( + String accessToken, + String refreshToken +) { + +} diff --git a/src/main/java/com/integrated/techhub/auth/exception/GithubRefreshTokenNotFoundException.java b/src/main/java/com/integrated/techhub/auth/exception/GithubRefreshTokenNotFoundException.java new file mode 100644 index 0000000..7526a36 --- /dev/null +++ b/src/main/java/com/integrated/techhub/auth/exception/GithubRefreshTokenNotFoundException.java @@ -0,0 +1,14 @@ +package com.integrated.techhub.auth.exception; + +import com.integrated.techhub.common.exception.ErrorCode; +import com.integrated.techhub.common.exception.TechHubException; + +import static org.springframework.http.HttpStatus.NOT_FOUND; + +public class GithubRefreshTokenNotFoundException extends TechHubException { + + public GithubRefreshTokenNotFoundException() { + super(new ErrorCode(NOT_FOUND, "깃허브 리프레시 토큰을 찾을 수 없습니다. 다시 인증해주세요.")); + } + +} diff --git a/src/main/java/com/integrated/techhub/auth/presentation/AuthController.java b/src/main/java/com/integrated/techhub/auth/presentation/AuthController.java index 0e7e941..a1655fc 100644 --- a/src/main/java/com/integrated/techhub/auth/presentation/AuthController.java +++ b/src/main/java/com/integrated/techhub/auth/presentation/AuthController.java @@ -1,14 +1,17 @@ package com.integrated.techhub.auth.presentation; import com.integrated.techhub.auth.application.AuthService; -import com.integrated.techhub.auth.dto.SignUpRequest; +import com.integrated.techhub.auth.application.client.GithubRestTemplateClient; +import com.integrated.techhub.auth.application.client.dto.response.OAuthTokensResponse; +import com.integrated.techhub.auth.dto.request.LoginRequest; +import com.integrated.techhub.auth.dto.request.SignUpRequest; +import com.integrated.techhub.auth.dto.response.TokenResponse; +import com.integrated.techhub.common.auth.annotation.Auth; +import com.integrated.techhub.common.auth.resolver.AuthProperties; 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.bind.annotation.*; import java.net.URI; @@ -18,6 +21,7 @@ public class AuthController { private final AuthService authService; + private final GithubRestTemplateClient githubRestTemplateClient; @PostMapping("/sign-up") public ResponseEntity signUp(@RequestBody @Valid final SignUpRequest request) { @@ -25,4 +29,21 @@ public ResponseEntity signUp(@RequestBody @Valid final SignUpRequest reque return ResponseEntity.created(URI.create("/members/" + memberId)).build(); } + @GetMapping("/login") + public ResponseEntity login(@RequestBody @Valid final LoginRequest request) { + final TokenResponse tokens = authService.getTokens(request.email(), request.password()); + return ResponseEntity.ok().body(tokens); + } + + @PutMapping("/login/oauth2/code/github") + public ResponseEntity authorizeGithub( + @Auth AuthProperties authProperties, + @RequestParam final String code + ) { + final OAuthTokensResponse tokensResponse = githubRestTemplateClient.getGithubTokens(code); + final Long memberId = authProperties.memberId(); + authService.saveGithubTokens(tokensResponse.toAccessToken(memberId), tokensResponse.toRefreshToken(memberId)); + return ResponseEntity.ok().build(); + } + } diff --git a/src/main/java/com/integrated/techhub/auth/util/GithubApiConstants.java b/src/main/java/com/integrated/techhub/auth/util/GithubApiConstants.java new file mode 100644 index 0000000..ff942d1 --- /dev/null +++ b/src/main/java/com/integrated/techhub/auth/util/GithubApiConstants.java @@ -0,0 +1,46 @@ +package com.integrated.techhub.auth.util; + +public class GithubApiConstants { + + public static class Auth { + /* + * [GITHUB_TOKEN_URL, GET_NEW_ACCESS_TOKEN] + * https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app#generating-a-user-access-token-when-a-user-installs-your-app + * Exchange the code from the previous step for a user access token by making a POST request to this URL, + * along with the following query parameters: https://github.com/login/oauth/access_token + * */ + private static final String GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token"; + private static final String NEW_ACCESS_TOKEN_URL = "https://api.github.com/login/oauth/access_token?clientId=%s&client_secret=%s&grant_type=refresh_token&refresh_token=%s"; + } + + public static String getGithubTokenUrl() { + return Auth.GITHUB_TOKEN_URL; + } + + public static String getNewAccessTokenUrl(final String clientId, final String clientSecret, final String githubRefreshToken) { + return String.format(Auth.NEW_ACCESS_TOKEN_URL, clientId, clientSecret, githubRefreshToken); + } + + public static class Member { + /* + * [LIST_PULL_REQUEST_URL] + * https://docs.github.com/ko/rest/pulls/pulls?apiVersion=2022-11-28#list-pull-requests + * Draft pull requests are available in public repositories with GitHub Free and GitHub Free for organizations, + * GitHub Pro, and legacy per-repository billing plans, and in public and private repositories + * with GitHub Team and GitHub Enterprise Cloud. For more information, + * see GitHub's products in the GitHub Help documentation. + * */ + @Deprecated + public static final String MEMBER_INFO_URL = "https://api.github.com/user"; + public static final String LIST_PULL_REQUEST_URL = "https://api.github.com/repos/woowacourse/%s/pulls?state=all&page=%d"; + } + + public static String getMemberInfoUrl() { + return Member.MEMBER_INFO_URL; + } + + public static String getListPullRequestUrl(final String repoName, final int page) { + return String.format(Member.LIST_PULL_REQUEST_URL, repoName, page); + } + +} diff --git a/src/main/java/com/integrated/techhub/common/auth/annotation/Auth.java b/src/main/java/com/integrated/techhub/common/auth/annotation/Auth.java new file mode 100644 index 0000000..a44b03c --- /dev/null +++ b/src/main/java/com/integrated/techhub/common/auth/annotation/Auth.java @@ -0,0 +1,12 @@ +package com.integrated.techhub.common.auth.annotation; + +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 Auth { + +} diff --git a/src/main/java/com/integrated/techhub/auth/infra/encode/BCryptPasswordEncoder.java b/src/main/java/com/integrated/techhub/common/auth/encode/BCryptPasswordEncoder.java similarity index 90% rename from src/main/java/com/integrated/techhub/auth/infra/encode/BCryptPasswordEncoder.java rename to src/main/java/com/integrated/techhub/common/auth/encode/BCryptPasswordEncoder.java index f4024f4..b41a126 100644 --- a/src/main/java/com/integrated/techhub/auth/infra/encode/BCryptPasswordEncoder.java +++ b/src/main/java/com/integrated/techhub/common/auth/encode/BCryptPasswordEncoder.java @@ -1,4 +1,4 @@ -package com.integrated.techhub.auth.infra.encode; +package com.integrated.techhub.common.auth.encode; import com.integrated.techhub.auth.domain.PasswordEncoder; import org.mindrot.jbcrypt.BCrypt; diff --git a/src/main/java/com/integrated/techhub/common/auth/jwt/BearerTokenExtractor.java b/src/main/java/com/integrated/techhub/common/auth/jwt/BearerTokenExtractor.java new file mode 100644 index 0000000..dd10793 --- /dev/null +++ b/src/main/java/com/integrated/techhub/common/auth/jwt/BearerTokenExtractor.java @@ -0,0 +1,28 @@ +package com.integrated.techhub.common.auth.jwt; + +import com.integrated.techhub.common.auth.jwt.exception.TokenInvalidException; +import jakarta.servlet.http.HttpServletRequest; +import lombok.NoArgsConstructor; + +import static lombok.AccessLevel.PRIVATE; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; + +@NoArgsConstructor(access = PRIVATE) +public class BearerTokenExtractor { + + private static final String BEARER_TYPE = "bearer "; + private static final String BEARER_JWT_REGEX = "^bearer [A-Za-z0-9-_=]+\\.[A-Za-z0-9-_=]+\\.?[A-Za-z0-9-_.+/=]*$"; + + public static String extract(final HttpServletRequest request) { + final String BearerToken = request.getHeader(AUTHORIZATION); + validateBearerToken(BearerToken); + return BearerToken.replace(BEARER_TYPE, "").trim(); + } + + private static void validateBearerToken(final String bearerToken) { + if (bearerToken == null || !bearerToken.matches(BEARER_JWT_REGEX)) { + throw new TokenInvalidException(); + } + } + +} diff --git a/src/main/java/com/integrated/techhub/common/auth/jwt/JwtProperties.java b/src/main/java/com/integrated/techhub/common/auth/jwt/JwtProperties.java new file mode 100644 index 0000000..d1ba5a7 --- /dev/null +++ b/src/main/java/com/integrated/techhub/common/auth/jwt/JwtProperties.java @@ -0,0 +1,14 @@ +package com.integrated.techhub.common.auth.jwt; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "spring.auth.jwt") +public record JwtProperties ( + String header, + String secretKey, + Long accessExp, + Long refreshExp, + String prefix +) { + +} diff --git a/src/main/java/com/integrated/techhub/common/auth/jwt/JwtProvider.java b/src/main/java/com/integrated/techhub/common/auth/jwt/JwtProvider.java new file mode 100644 index 0000000..90a4f27 --- /dev/null +++ b/src/main/java/com/integrated/techhub/common/auth/jwt/JwtProvider.java @@ -0,0 +1,67 @@ +package com.integrated.techhub.common.auth.jwt; + +import com.integrated.techhub.auth.domain.RefreshToken; +import com.integrated.techhub.auth.domain.repository.RefreshTokenRepository; +import com.integrated.techhub.auth.domain.type.Type; +import com.integrated.techhub.common.auth.jwt.exception.TokenInvalidException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Date; + +import static com.integrated.techhub.auth.domain.type.Type.TECHHUB; +import static io.jsonwebtoken.SignatureAlgorithm.HS256; + +@Component +@RequiredArgsConstructor +public class JwtProvider { + + private final JwtProperties jwtProperties; + private final RefreshTokenRepository refreshTokenRepository; + + public String generateAccessToken(final Long memberId) { + return generateToken(memberId, jwtProperties.accessExp()); + } + + public RefreshToken generateRefreshToken(final Long memberId) { + final String refreshToken = generateToken(memberId, jwtProperties.refreshExp()); + return refreshTokenRepository.save(RefreshToken.builder() + .memberId(memberId) + .token(refreshToken) + .type(TECHHUB) + .ttl(jwtProperties.refreshExp()) + .build()); + } + + private String generateToken(final Long memberId, final Long tokenExp) { + return Jwts.builder() + .setSubject(String.valueOf(memberId)) + .claim("memberId", memberId) + .signWith(HS256, jwtProperties.secretKey()) + .setExpiration(new Date(System.currentTimeMillis() + tokenExp * 1000)) + .setIssuedAt(new Date()) + .compact(); + } + + public Long getPayload(final String token) { + return Long.valueOf(validateParseJws(token).getBody().getSubject()); + } + + public Jws validateParseJws(final String token) { + try { + return Jwts.parserBuilder() + .setSigningKey(jwtProperties.secretKey()) + .build() + .parseClaimsJws(token); + } catch (ExpiredJwtException e) { + throw new IllegalArgumentException("토큰 만료임"); + } catch (Exception e) { + throw new TokenInvalidException(); + } + } + +} diff --git a/src/main/java/com/integrated/techhub/common/auth/jwt/exception/TokenInvalidException.java b/src/main/java/com/integrated/techhub/common/auth/jwt/exception/TokenInvalidException.java new file mode 100644 index 0000000..9648213 --- /dev/null +++ b/src/main/java/com/integrated/techhub/common/auth/jwt/exception/TokenInvalidException.java @@ -0,0 +1,14 @@ +package com.integrated.techhub.common.auth.jwt.exception; + +import com.integrated.techhub.common.exception.ErrorCode; +import com.integrated.techhub.common.exception.TechHubException; + +import static org.springframework.http.HttpStatus.UNAUTHORIZED; + +public class TokenInvalidException extends TechHubException { + + public TokenInvalidException() { + super(new ErrorCode(UNAUTHORIZED, "유효하지 않은 토큰입니다. 유효한 토큰인지 확인해주세요.")); + } + +} diff --git a/src/main/java/com/integrated/techhub/common/auth/resolver/AuthArgumentResolver.java b/src/main/java/com/integrated/techhub/common/auth/resolver/AuthArgumentResolver.java new file mode 100644 index 0000000..00eb1e1 --- /dev/null +++ b/src/main/java/com/integrated/techhub/common/auth/resolver/AuthArgumentResolver.java @@ -0,0 +1,42 @@ +package com.integrated.techhub.common.auth.resolver; + +import com.integrated.techhub.common.auth.annotation.Auth; +import com.integrated.techhub.common.auth.jwt.BearerTokenExtractor; +import com.integrated.techhub.common.auth.jwt.JwtProvider; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +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 java.util.Objects; + +@Component +@RequiredArgsConstructor +public class AuthArgumentResolver implements HandlerMethodArgumentResolver { + + private final JwtProvider jwtProvider; + + @Override + public boolean supportsParameter(final MethodParameter parameter) { + return parameter.hasParameterAnnotation(Auth.class) + && parameter.getParameterType().equals(AuthProperties.class); + } + + @Override + public Object resolveArgument( + final MethodParameter parameter, + final ModelAndViewContainer mavContainer, + final NativeWebRequest webRequest, + final WebDataBinderFactory binderFactory + ) { + final HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + final String token = BearerTokenExtractor.extract(Objects.requireNonNull(request)); + final Long memberId = jwtProvider.getPayload(token); + return new AuthProperties(memberId); + } + +} diff --git a/src/main/java/com/integrated/techhub/common/auth/resolver/AuthProperties.java b/src/main/java/com/integrated/techhub/common/auth/resolver/AuthProperties.java new file mode 100644 index 0000000..d3b9cee --- /dev/null +++ b/src/main/java/com/integrated/techhub/common/auth/resolver/AuthProperties.java @@ -0,0 +1,6 @@ +package com.integrated.techhub.common.auth.resolver; + +public record AuthProperties( + Long memberId +) { +} diff --git a/src/main/java/com/integrated/techhub/common/config/EmbeddedRedisConfig.java b/src/main/java/com/integrated/techhub/common/config/EmbeddedRedisConfig.java index 25032f9..7be310e 100644 --- a/src/main/java/com/integrated/techhub/common/config/EmbeddedRedisConfig.java +++ b/src/main/java/com/integrated/techhub/common/config/EmbeddedRedisConfig.java @@ -4,7 +4,6 @@ import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; import org.springframework.util.StringUtils; import redis.embedded.RedisServer; @@ -13,7 +12,7 @@ import java.io.InputStreamReader; @Slf4j -@Configuration +//@Configuration public class EmbeddedRedisConfig { @Value("${spring.data.redis.port}") diff --git a/src/main/java/com/integrated/techhub/common/config/PropertiesConfig.java b/src/main/java/com/integrated/techhub/common/config/PropertiesConfig.java new file mode 100644 index 0000000..902eeff --- /dev/null +++ b/src/main/java/com/integrated/techhub/common/config/PropertiesConfig.java @@ -0,0 +1,15 @@ +package com.integrated.techhub.common.config; + +import com.integrated.techhub.auth.application.client.GithubClientProperties; +import com.integrated.techhub.common.auth.jwt.JwtProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties({ + JwtProperties.class, + GithubClientProperties.class +}) +public class PropertiesConfig { + +} diff --git a/src/main/java/com/integrated/techhub/common/config/WebConfig.java b/src/main/java/com/integrated/techhub/common/config/WebConfig.java new file mode 100644 index 0000000..84ad149 --- /dev/null +++ b/src/main/java/com/integrated/techhub/common/config/WebConfig.java @@ -0,0 +1,38 @@ +package com.integrated.techhub.common.config; + +import com.integrated.techhub.common.auth.jwt.JwtProvider; +import com.integrated.techhub.common.auth.resolver.AuthArgumentResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +import static org.springframework.http.HttpHeaders.LOCATION; + +@Component +@RequiredArgsConstructor +public class WebConfig implements WebMvcConfigurer { + + private static final String ALLOW_ALL_PATH = "/**"; + private static final String ALLOWED_METHODS = "*"; + private static final String FRONTEND_LOCALHOST = "http://localhost:3000"; + + private final JwtProvider jwtProvider; + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping(ALLOW_ALL_PATH) + .allowedMethods(ALLOWED_METHODS) + .allowedOrigins(FRONTEND_LOCALHOST) + .exposedHeaders(LOCATION); + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(new AuthArgumentResolver(jwtProvider)); + } + +} diff --git a/src/main/java/com/integrated/techhub/mail/application/MailService.java b/src/main/java/com/integrated/techhub/mail/application/MailService.java index 8dcf74a..cccf3b3 100644 --- a/src/main/java/com/integrated/techhub/mail/application/MailService.java +++ b/src/main/java/com/integrated/techhub/mail/application/MailService.java @@ -36,7 +36,7 @@ public class MailService { private final ITemplateEngine templateEngine; private final AuthorityCodeRepository authorityCodeRepository; - @Async + @Async("MailExecutor") public void sendMail(final MailSendRequest request) { final int randomAuthCode = createRandomAuthCode(); final String template = createTemplate(String.valueOf(randomAuthCode)); diff --git a/src/main/java/com/integrated/techhub/mail/exception/AuthorityCodeNotMatchException.java b/src/main/java/com/integrated/techhub/mail/exception/AuthorityCodeNotMatchException.java index 6ac3695..232b99c 100644 --- a/src/main/java/com/integrated/techhub/mail/exception/AuthorityCodeNotMatchException.java +++ b/src/main/java/com/integrated/techhub/mail/exception/AuthorityCodeNotMatchException.java @@ -4,7 +4,6 @@ import com.integrated.techhub.common.exception.TechHubException; import static org.springframework.http.HttpStatus.BAD_REQUEST; -import static org.springframework.http.HttpStatus.NOT_FOUND; public class AuthorityCodeNotMatchException extends TechHubException { diff --git a/src/main/java/com/integrated/techhub/mail/presentation/MailController.java b/src/main/java/com/integrated/techhub/mail/presentation/MailController.java index 1775513..e76c161 100644 --- a/src/main/java/com/integrated/techhub/mail/presentation/MailController.java +++ b/src/main/java/com/integrated/techhub/mail/presentation/MailController.java @@ -1,8 +1,8 @@ package com.integrated.techhub.mail.presentation; import com.integrated.techhub.auth.application.AuthQueryService; -import com.integrated.techhub.mail.dto.MailSendRequest; import com.integrated.techhub.mail.application.MailService; +import com.integrated.techhub.mail.dto.MailSendRequest; import com.integrated.techhub.mail.dto.MailValidateRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/integrated/techhub/member/domain/Member.java b/src/main/java/com/integrated/techhub/member/domain/Member.java index dd94ac7..b2762a6 100644 --- a/src/main/java/com/integrated/techhub/member/domain/Member.java +++ b/src/main/java/com/integrated/techhub/member/domain/Member.java @@ -1,11 +1,8 @@ package com.integrated.techhub.member.domain; import com.integrated.techhub.auth.domain.PasswordEncoder; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; +import com.integrated.techhub.member.exception.PasswordNotMatchException; +import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -51,8 +48,13 @@ public class Member { @Enumerated(value = STRING) private Position position; - public void encodePassword(PasswordEncoder passwordEncoder) { + public void encodePassword(final PasswordEncoder passwordEncoder) { this.password = passwordEncoder.encode(this.password); } + public void validateMatchPassword(final PasswordEncoder passwordEncoder, final String requestPassword) { + if (!passwordEncoder.isMatch(requestPassword, this.password)) { + throw new PasswordNotMatchException(); + } + } } diff --git a/src/main/java/com/integrated/techhub/member/domain/repository/MemberRepository.java b/src/main/java/com/integrated/techhub/member/domain/repository/MemberRepository.java index 4d20c4b..f3d3c19 100644 --- a/src/main/java/com/integrated/techhub/member/domain/repository/MemberRepository.java +++ b/src/main/java/com/integrated/techhub/member/domain/repository/MemberRepository.java @@ -1,11 +1,25 @@ package com.integrated.techhub.member.domain.repository; import com.integrated.techhub.member.domain.Member; +import com.integrated.techhub.member.exception.MemberNotFoundException; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface MemberRepository extends JpaRepository { - boolean existsByEmail(String email); + boolean existsByEmail(final String email); + + Optional findByEmail(final String email); + + default Member getById(final Long id) { + return findById(id) + .orElseThrow(() -> new MemberNotFoundException(id)); + } + default Member getByEmail(final String email) { + return findByEmail(email) + .orElseThrow(() -> new MemberNotFoundException(email)); + } } diff --git a/src/main/java/com/integrated/techhub/member/exception/MemberExistsException.java b/src/main/java/com/integrated/techhub/member/exception/MemberAlreadyExistsException.java similarity index 74% rename from src/main/java/com/integrated/techhub/member/exception/MemberExistsException.java rename to src/main/java/com/integrated/techhub/member/exception/MemberAlreadyExistsException.java index e9152a0..89aead0 100644 --- a/src/main/java/com/integrated/techhub/member/exception/MemberExistsException.java +++ b/src/main/java/com/integrated/techhub/member/exception/MemberAlreadyExistsException.java @@ -5,9 +5,9 @@ import static org.springframework.http.HttpStatus.BAD_REQUEST; -public class MemberExistsException extends TechHubException { +public class MemberAlreadyExistsException extends TechHubException { - public MemberExistsException() { + public MemberAlreadyExistsException() { super(new ErrorCode(BAD_REQUEST, "이미 존재하는 회원입니다.")); } diff --git a/src/main/java/com/integrated/techhub/member/exception/MemberNotFoundException.java b/src/main/java/com/integrated/techhub/member/exception/MemberNotFoundException.java new file mode 100644 index 0000000..c54efff --- /dev/null +++ b/src/main/java/com/integrated/techhub/member/exception/MemberNotFoundException.java @@ -0,0 +1,18 @@ +package com.integrated.techhub.member.exception; + +import com.integrated.techhub.common.exception.ErrorCode; +import com.integrated.techhub.common.exception.TechHubException; + +import static org.springframework.http.HttpStatus.NOT_FOUND; + +public class MemberNotFoundException extends TechHubException { + + public MemberNotFoundException(final Long id) { + super(new ErrorCode(NOT_FOUND, String.format("id가 %d인 유저를 찾을 수 없습니다.", id))); + } + + public MemberNotFoundException(final String email) { + super(new ErrorCode(NOT_FOUND, String.format("email이 %s인 유저를 찾을 수 없습니다.", email))); + } + +} diff --git a/src/main/java/com/integrated/techhub/member/exception/PasswordNotMatchException.java b/src/main/java/com/integrated/techhub/member/exception/PasswordNotMatchException.java new file mode 100644 index 0000000..81ac07e --- /dev/null +++ b/src/main/java/com/integrated/techhub/member/exception/PasswordNotMatchException.java @@ -0,0 +1,14 @@ +package com.integrated.techhub.member.exception; + +import com.integrated.techhub.common.exception.ErrorCode; +import com.integrated.techhub.common.exception.TechHubException; + +import static org.springframework.http.HttpStatus.FORBIDDEN; + +public class PasswordNotMatchException extends TechHubException { + + public PasswordNotMatchException() { + super(new ErrorCode(FORBIDDEN, "유저의 비밀번호가 일치하지 않습니다.")); + } + +} diff --git a/src/main/java/com/integrated/techhub/mission/domain/Mission.java b/src/main/java/com/integrated/techhub/mission/domain/Mission.java new file mode 100644 index 0000000..2bdc174 --- /dev/null +++ b/src/main/java/com/integrated/techhub/mission/domain/Mission.java @@ -0,0 +1,34 @@ +package com.integrated.techhub.mission.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = PROTECTED) +public class Mission { + + @Id @GeneratedValue(strategy = IDENTITY) + private Long id; + + @Column(length = 64, nullable = false) + private String title; + + @Column(nullable = false) + private String repoName; + + @Column(nullable = false) + private String repoUrl; + +} diff --git a/src/main/java/com/integrated/techhub/mission/domain/Step.java b/src/main/java/com/integrated/techhub/mission/domain/Step.java new file mode 100644 index 0000000..5ff05dc --- /dev/null +++ b/src/main/java/com/integrated/techhub/mission/domain/Step.java @@ -0,0 +1,30 @@ +package com.integrated.techhub.mission.domain; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = PROTECTED) +public class Step { + + @Id @GeneratedValue(strategy = IDENTITY) + private Long id; + + @Column(nullable = false) + private Long number; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "mission_id") + private Mission mission; + +} diff --git a/src/main/java/com/integrated/techhub/mission/domain/exception/StepNotFoundException.java b/src/main/java/com/integrated/techhub/mission/domain/exception/StepNotFoundException.java new file mode 100644 index 0000000..91c6dbb --- /dev/null +++ b/src/main/java/com/integrated/techhub/mission/domain/exception/StepNotFoundException.java @@ -0,0 +1,17 @@ +package com.integrated.techhub.mission.domain.exception; + +import com.integrated.techhub.common.exception.ErrorCode; +import com.integrated.techhub.common.exception.TechHubException; +import org.springframework.http.HttpStatus; + +public class StepNotFoundException extends TechHubException { + + public StepNotFoundException(final String title) { + super(new ErrorCode(HttpStatus.NOT_FOUND, String.format("%s인 PR 제목에서 Step을 찾을 수 없습니다. 제목에 Step이 존재하는지 확인해주세요.", title))); + } + + public StepNotFoundException(Long stepNumber) { + super(new ErrorCode(HttpStatus.NOT_FOUND, String.format("미션에서 step%d를 찾을 수 없습니다. 해당 step이 존재하는지 확인해주세요.", stepNumber))); + } + +} diff --git a/src/main/java/com/integrated/techhub/mission/domain/repository/StepRepository.java b/src/main/java/com/integrated/techhub/mission/domain/repository/StepRepository.java new file mode 100644 index 0000000..ad34876 --- /dev/null +++ b/src/main/java/com/integrated/techhub/mission/domain/repository/StepRepository.java @@ -0,0 +1,17 @@ +package com.integrated.techhub.mission.domain.repository; + +import com.integrated.techhub.mission.domain.Step; +import com.integrated.techhub.mission.domain.exception.StepNotFoundException; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface StepRepository extends JpaRepository { + + Optional findByNumber(final Long stepNumber); + + default Step getByNumber(final Long stepNumber) { + return findByNumber(stepNumber) + .orElseThrow(() -> new StepNotFoundException(stepNumber)); + } +} diff --git a/src/main/java/com/integrated/techhub/mission/domain/type/StepStatus.java b/src/main/java/com/integrated/techhub/mission/domain/type/StepStatus.java new file mode 100644 index 0000000..444b3fd --- /dev/null +++ b/src/main/java/com/integrated/techhub/mission/domain/type/StepStatus.java @@ -0,0 +1,8 @@ +package com.integrated.techhub.mission.domain.type; + +public enum StepStatus { + + COMPLETE, + INCOMPLETE + +} diff --git a/src/main/java/com/integrated/techhub/pr/application/PullRequestQueryService.java b/src/main/java/com/integrated/techhub/pr/application/PullRequestQueryService.java index d21e2f5..3cbdd04 100644 --- a/src/main/java/com/integrated/techhub/pr/application/PullRequestQueryService.java +++ b/src/main/java/com/integrated/techhub/pr/application/PullRequestQueryService.java @@ -1,8 +1,8 @@ package com.integrated.techhub.pr.application; -import com.integrated.techhub.pr.domain.PullRequest; -import com.integrated.techhub.pr.domain.repository.PullRequestRepository; -import com.integrated.techhub.pr.dto.response.GetPullRequestResponse; +import com.integrated.techhub.pr.dto.response.PullRequestResponse; +import com.integrated.techhub.pr.infra.dto.PullRequestQueryResponse; +import com.integrated.techhub.pr.infra.persist.PullRequestQueryRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,12 +14,12 @@ @Transactional(readOnly = true) public class PullRequestQueryService { - private final PullRequestRepository pullRequestRepository; + private final PullRequestQueryRepository pullRequestQueryRepository; - public List getPullRequestByMemberId(Long memberId) { - List pullRequests = pullRequestRepository.findByMemberId(memberId); - return pullRequests.stream() - .map(GetPullRequestResponse::of) + public List getMyPullRequestsByMissionId(final Long memberId, final Long missionId) { + final List pullRequestsQueryResponses = pullRequestQueryRepository.findByMemberIdAndMissionId(memberId, missionId); + return pullRequestsQueryResponses.stream() + .map(PullRequestResponse::from) .toList(); } diff --git a/src/main/java/com/integrated/techhub/pr/application/PullRequestService.java b/src/main/java/com/integrated/techhub/pr/application/PullRequestService.java new file mode 100644 index 0000000..94c0472 --- /dev/null +++ b/src/main/java/com/integrated/techhub/pr/application/PullRequestService.java @@ -0,0 +1,62 @@ +package com.integrated.techhub.pr.application; + +import com.integrated.techhub.auth.application.client.dto.response.GithubPrInfoResponse; +import com.integrated.techhub.mission.domain.Step; +import com.integrated.techhub.mission.domain.exception.StepNotFoundException; +import com.integrated.techhub.mission.domain.repository.StepRepository; +import com.integrated.techhub.pr.domain.PullRequest; +import com.integrated.techhub.pr.domain.repository.PullRequestRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Service +@Transactional +@RequiredArgsConstructor +public class PullRequestService { + + private final StepRepository stepRepository; + private final PullRequestRepository pullRequestRepository; + + // TODO: Require Refactor + public void create(final Long memberId, final List prsByRepoName) { + final List prs = new ArrayList<>(); + for (GithubPrInfoResponse pr : prsByRepoName) { + final List stepsInTitle = getStepInTitle(pr.title()); + for (Long stepNumber : stepsInTitle) { + final Step step = stepRepository.getByNumber(stepNumber); + final PullRequest pullRequest = pr.toPullRequest(memberId, step.getId()); + prs.add(pullRequest); + } + } + isNotExistSave(prs); + } + + private List getStepInTitle(final String title) { + final List steps = new ArrayList<>(); + final Pattern pattern = Pattern.compile("\\d+"); + final Matcher matcher = pattern.matcher(title); + while (matcher.find()) { + final Long step = Long.valueOf(matcher.group()); + steps.add(step); + } + if (steps.isEmpty()) { + throw new StepNotFoundException(title); + } + return steps; + } + + private void isNotExistSave(final List newPrs) { + for (PullRequest newPr : newPrs) { + if (!pullRequestRepository.existsByTitle(newPr.getTitle())) { + pullRequestRepository.save(newPr); + } + } + } + +} diff --git a/src/main/java/com/integrated/techhub/pr/domain/PullRequest.java b/src/main/java/com/integrated/techhub/pr/domain/PullRequest.java index 61b53fd..8844aaa 100644 --- a/src/main/java/com/integrated/techhub/pr/domain/PullRequest.java +++ b/src/main/java/com/integrated/techhub/pr/domain/PullRequest.java @@ -1,6 +1,6 @@ package com.integrated.techhub.pr.domain; -import com.integrated.techhub.pr.domain.type.Status; +import com.integrated.techhub.pr.domain.type.State; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; @@ -26,12 +26,16 @@ public class PullRequest { private Long memberId; @Column(nullable = false) - private Long missionId; + private Long stepId; - @Column(nullable = false, length = 255) + @Column(nullable = false) private String title; @Enumerated(value = STRING) - private Status status; + private State state; + public void update(final String title, final State state) { + this.title = title; + this.state = state; + } } diff --git a/src/main/java/com/integrated/techhub/pr/domain/repository/PullRequestQueryRepository.java b/src/main/java/com/integrated/techhub/pr/domain/repository/PullRequestQueryRepository.java deleted file mode 100644 index 3fcc1b4..0000000 --- a/src/main/java/com/integrated/techhub/pr/domain/repository/PullRequestQueryRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.integrated.techhub.pr.domain.repository; - -import com.integrated.techhub.pr.domain.PullRequest; - -import java.util.List; - -public interface PullRequestQueryRepository { - - List findPullRequestByMemberId(Long memberId); - -} diff --git a/src/main/java/com/integrated/techhub/pr/domain/repository/PullRequestRepository.java b/src/main/java/com/integrated/techhub/pr/domain/repository/PullRequestRepository.java index e91371c..9bb981f 100644 --- a/src/main/java/com/integrated/techhub/pr/domain/repository/PullRequestRepository.java +++ b/src/main/java/com/integrated/techhub/pr/domain/repository/PullRequestRepository.java @@ -3,10 +3,8 @@ import com.integrated.techhub.pr.domain.PullRequest; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - public interface PullRequestRepository extends JpaRepository { - List findByMemberId(Long memberId); + boolean existsByTitle(final String title); } diff --git a/src/main/java/com/integrated/techhub/pr/domain/type/Status.java b/src/main/java/com/integrated/techhub/pr/domain/type/State.java similarity index 50% rename from src/main/java/com/integrated/techhub/pr/domain/type/Status.java rename to src/main/java/com/integrated/techhub/pr/domain/type/State.java index 6b753ce..26dcd5d 100644 --- a/src/main/java/com/integrated/techhub/pr/domain/type/Status.java +++ b/src/main/java/com/integrated/techhub/pr/domain/type/State.java @@ -1,9 +1,11 @@ package com.integrated.techhub.pr.domain.type; -public enum Status { +import lombok.Getter; + +@Getter +public enum State { OPEN, - CLOSED, - MERGED + CLOSED } diff --git a/src/main/java/com/integrated/techhub/pr/dto/response/GetPullRequestResponse.java b/src/main/java/com/integrated/techhub/pr/dto/response/GetPullRequestResponse.java deleted file mode 100644 index db1c909..0000000 --- a/src/main/java/com/integrated/techhub/pr/dto/response/GetPullRequestResponse.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.integrated.techhub.pr.dto.response; - -import com.integrated.techhub.pr.domain.PullRequest; -import lombok.Builder; - -public record GetPullRequestResponse( - Long id, - Long memberId, - Long missionId, - String title, - String status -) { - - @Builder - public GetPullRequestResponse { - } - - public static GetPullRequestResponse of(final PullRequest request) { - return GetPullRequestResponse.builder() - .id(request.getId()) - .memberId(request.getMemberId()) - .missionId(request.getMissionId()) - .title(request.getTitle()) - .status(request.getStatus().name()) - .build(); - } - -} diff --git a/src/main/java/com/integrated/techhub/pr/dto/response/PullRequestResponse.java b/src/main/java/com/integrated/techhub/pr/dto/response/PullRequestResponse.java new file mode 100644 index 0000000..46da0f4 --- /dev/null +++ b/src/main/java/com/integrated/techhub/pr/dto/response/PullRequestResponse.java @@ -0,0 +1,20 @@ +package com.integrated.techhub.pr.dto.response; + +import com.integrated.techhub.pr.infra.dto.PullRequestQueryResponse; + +public record PullRequestResponse ( + Long id, + Long step, + String title, + String status +) { + + public static PullRequestResponse from(final PullRequestQueryResponse response) { + return new PullRequestResponse( + response.id(), + response.step(), + response.title(), + response.status() + ); + } +} diff --git a/src/main/java/com/integrated/techhub/pr/infra/dto/PullRequestQueryResponse.java b/src/main/java/com/integrated/techhub/pr/infra/dto/PullRequestQueryResponse.java new file mode 100644 index 0000000..65f27f2 --- /dev/null +++ b/src/main/java/com/integrated/techhub/pr/infra/dto/PullRequestQueryResponse.java @@ -0,0 +1,16 @@ +package com.integrated.techhub.pr.infra.dto; + +import com.querydsl.core.annotations.QueryProjection; + +public record PullRequestQueryResponse( + Long id, + Long step, + String title, + String status +) { + + @QueryProjection + public PullRequestQueryResponse { + } + +} diff --git a/src/main/java/com/integrated/techhub/pr/infra/persist/PullRequestQueryRepository.java b/src/main/java/com/integrated/techhub/pr/infra/persist/PullRequestQueryRepository.java new file mode 100644 index 0000000..e3b86d1 --- /dev/null +++ b/src/main/java/com/integrated/techhub/pr/infra/persist/PullRequestQueryRepository.java @@ -0,0 +1,11 @@ +package com.integrated.techhub.pr.infra.persist; + +import com.integrated.techhub.pr.infra.dto.PullRequestQueryResponse; + +import java.util.List; + +public interface PullRequestQueryRepository { + + List findByMemberIdAndMissionId(Long memberId, Long missionId); + +} diff --git a/src/main/java/com/integrated/techhub/pr/infra/persist/PullRequestQueryRepositoryImpl.java b/src/main/java/com/integrated/techhub/pr/infra/persist/PullRequestQueryRepositoryImpl.java new file mode 100644 index 0000000..8c94d0e --- /dev/null +++ b/src/main/java/com/integrated/techhub/pr/infra/persist/PullRequestQueryRepositoryImpl.java @@ -0,0 +1,41 @@ +package com.integrated.techhub.pr.infra.persist; + +import com.integrated.techhub.pr.infra.dto.PullRequestQueryResponse; +import com.integrated.techhub.pr.infra.dto.QPullRequestQueryResponse; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static com.integrated.techhub.mission.domain.QMission.mission; +import static com.integrated.techhub.mission.domain.QStep.step; +import static com.integrated.techhub.pr.domain.QPullRequest.pullRequest; + +@Repository +@RequiredArgsConstructor +public class PullRequestQueryRepositoryImpl implements PullRequestQueryRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public List findByMemberIdAndMissionId(final Long memberId, final Long missionId) { + return queryFactory.select(new QPullRequestQueryResponse( + pullRequest.id, + step.number, + pullRequest.title, + pullRequest.state.stringValue() + )) + .from(pullRequest) + .innerJoin(step).on(pullRequest.stepId.eq(step.id)) + .innerJoin(step.mission, mission) + .fetchJoin() + .where( + mission.id.eq(step.mission.id), + pullRequest.memberId.eq(memberId), + mission.id.eq(missionId) + ) + .fetch(); + } + +} diff --git a/src/main/java/com/integrated/techhub/pr/presentation/PullRequestController.java b/src/main/java/com/integrated/techhub/pr/presentation/PullRequestController.java index 48b37ac..0266b73 100644 --- a/src/main/java/com/integrated/techhub/pr/presentation/PullRequestController.java +++ b/src/main/java/com/integrated/techhub/pr/presentation/PullRequestController.java @@ -1,13 +1,17 @@ package com.integrated.techhub.pr.presentation; +import com.integrated.techhub.auth.application.client.GithubClientQueryService; +import com.integrated.techhub.auth.application.client.dto.response.GithubPrInfoResponse; +import com.integrated.techhub.common.auth.annotation.Auth; +import com.integrated.techhub.common.auth.resolver.AuthProperties; +import com.integrated.techhub.member.domain.Member; +import com.integrated.techhub.member.domain.repository.MemberRepository; import com.integrated.techhub.pr.application.PullRequestQueryService; -import com.integrated.techhub.pr.dto.response.GetPullRequestResponse; +import com.integrated.techhub.pr.application.PullRequestService; +import com.integrated.techhub.pr.dto.response.PullRequestResponse; 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 org.springframework.web.bind.annotation.*; import java.util.List; @@ -16,11 +20,32 @@ @RequiredArgsConstructor public class PullRequestController { + private final MemberRepository memberRepository; + private final PullRequestService pullRequestService; private final PullRequestQueryService pullRequestQueryService; + private final GithubClientQueryService githubClientQueryService; - @GetMapping("/{memberId}") - public ResponseEntity> searchPullRequest(@PathVariable Long memberId) { - List pullRequests = pullRequestQueryService.getPullRequestByMemberId(memberId); - return ResponseEntity.ok().body(pullRequests); + @GetMapping("/mine/{missionId}") + public ResponseEntity> getLoginUserPullRequestsByMissionId( + @Auth final AuthProperties authProperties, + @PathVariable final Long missionId + ) { + final List responses = pullRequestQueryService.getMyPullRequestsByMissionId(authProperties.memberId(), missionId); + return ResponseEntity.ok(responses); } + + @PutMapping("/sync/mine") + public ResponseEntity> syncMyPrsByRepoName( + @Auth final AuthProperties authProperties, + @RequestParam final String repoName + ) { + final Member member = memberRepository.getById(authProperties.memberId()); + final List allPrsByRepoName = githubClientQueryService.getPrsByRepoName(authProperties.memberId(), repoName); + final List myPrs = allPrsByRepoName.stream() + .filter(pr -> pr.title().contains(member.getNickname())) + .toList(); + pullRequestService.create(authProperties.memberId(), myPrs); + return ResponseEntity.ok().build(); + } + } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 1d5f486..6eab06c 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -7,7 +7,6 @@ server: # database spring: config: - import: classpath:/database-env.properties datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: ${JDBC_URL} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 4683c9d..0bc75ed 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -20,7 +20,7 @@ spring: database-platform: org.hibernate.dialect.MySQL57Dialect generate-ddl: true hibernate: - ddl-auto: create + ddl-auto: update properties: hibernate: format_sql: true diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 1d5f486..6eab06c 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -7,7 +7,6 @@ server: # database spring: config: - import: classpath:/database-env.properties datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: ${JDBC_URL} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a86bc9f..bee5339 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,7 +4,8 @@ spring: redis: host: localhost port: ${REDIS_OUT_BOUND_PORT} - + main: + allow-bean-definition-overriding: true --- # application spring: @@ -14,12 +15,10 @@ spring: --- # jwt spring: - config: - import: classpath:/jwt-env.properties auth: jwt: header: ${JWT_HEADER} - secret: ${JWT_SECRET} + secretKey: ${JWT_SECRET} accessExp: ${JWT_ACCESS_EXP} refreshExp: ${JWT_REFRESH_EXP} prefix: ${JWT_PREFIX} @@ -38,4 +37,15 @@ spring: auth: true starttls: enable: true - required: true \ No newline at end of file + required: true + +--- +# oauth 2.0 +spring: + security: + oauth2: + client: + registration: + github: + client-id: ${LOCAL_GITHUB_CLIENT_ID} + client-secret: ${LOCAL_GITHUB_CLIENT_SECRET} \ No newline at end of file diff --git a/src/main/resources/database-env.properties b/src/main/resources/database-env.properties new file mode 100644 index 0000000..abc5b11 --- /dev/null +++ b/src/main/resources/database-env.properties @@ -0,0 +1,3 @@ +JDBC_URL=jdbc:mysql://localhost:13306/techhub +DB_USERNAME=techhub +DB_PASSWORD=techhub!@ \ No newline at end of file diff --git a/src/main/resources/jwt-env.properties b/src/main/resources/jwt-env.properties new file mode 100644 index 0000000..8dc9848 --- /dev/null +++ b/src/main/resources/jwt-env.properties @@ -0,0 +1,5 @@ +JWT_PREFIX=bearer +JWT_HEADER=Authorization +JWT_ACCESS_EXP=1800 +JWT_REFRESH_EXP=2592000 +JWT_SECRET=4431f04705zip.78d361ea58a5go.229b1958ba diff --git a/src/test/java/com/integrated/techhub/auth/application/AuthQueryServiceTest.java b/src/test/java/com/integrated/techhub/auth/application/AuthQueryServiceTest.java index c4642da..f184e85 100644 --- a/src/test/java/com/integrated/techhub/auth/application/AuthQueryServiceTest.java +++ b/src/test/java/com/integrated/techhub/auth/application/AuthQueryServiceTest.java @@ -2,7 +2,7 @@ import com.integrated.techhub.member.domain.Member; import com.integrated.techhub.member.domain.repository.MemberRepository; -import com.integrated.techhub.member.exception.MemberExistsException; +import com.integrated.techhub.member.exception.MemberAlreadyExistsException; import com.integrated.techhub.member.fixture.MemberFixture; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -27,7 +27,7 @@ void validateExistedMember() { Member member = memberRepository.save(MemberFixture.무민); //when, then - assertThrows(MemberExistsException.class, () -> authQueryService.validateExistedMember(member.getEmail())); + assertThrows(MemberAlreadyExistsException.class, () -> authQueryService.validateExistedMember(member.getEmail())); } } \ No newline at end of file diff --git a/src/test/java/com/integrated/techhub/auth/application/AuthServiceTest.java b/src/test/java/com/integrated/techhub/auth/application/AuthServiceTest.java index 13d8346..8f0fb69 100644 --- a/src/test/java/com/integrated/techhub/auth/application/AuthServiceTest.java +++ b/src/test/java/com/integrated/techhub/auth/application/AuthServiceTest.java @@ -1,6 +1,6 @@ package com.integrated.techhub.auth.application; -import com.integrated.techhub.auth.dto.SignUpRequest; +import com.integrated.techhub.auth.dto.request.SignUpRequest; import com.integrated.techhub.member.domain.repository.MemberRepository; import com.integrated.techhub.member.fixture.MemberFixture; import org.assertj.core.api.Assertions; diff --git a/src/test/java/com/integrated/techhub/auth/infra/encode/BCryptPasswordEncoderTest.java b/src/test/java/com/integrated/techhub/auth/infra/encode/BCryptPasswordEncoderTest.java index 42abe79..0cf24b4 100644 --- a/src/test/java/com/integrated/techhub/auth/infra/encode/BCryptPasswordEncoderTest.java +++ b/src/test/java/com/integrated/techhub/auth/infra/encode/BCryptPasswordEncoderTest.java @@ -1,5 +1,6 @@ package com.integrated.techhub.auth.infra.encode; +import com.integrated.techhub.common.auth.encode.BCryptPasswordEncoder; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/src/test/java/com/integrated/techhub/mail/presentation/MailControllerMockTest.java b/src/test/java/com/integrated/techhub/mail/presentation/MailControllerMockTest.java index f3d12ed..dc7bbea 100644 --- a/src/test/java/com/integrated/techhub/mail/presentation/MailControllerMockTest.java +++ b/src/test/java/com/integrated/techhub/mail/presentation/MailControllerMockTest.java @@ -1,55 +1,55 @@ -package com.integrated.techhub.mail.presentation; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.integrated.techhub.auth.application.AuthQueryService; -import com.integrated.techhub.mail.application.MailService; -import com.integrated.techhub.member.fixture.MemberFixture; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.MediaType; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.web.servlet.MockMvc; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - - -@ExtendWith(SpringExtension.class) -@SuppressWarnings("NonAsciiCharacters") -@WebMvcTest(controllers = MailController.class) -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class MailControllerMockTest { - - @Autowired - private ObjectMapper objectMapper; - - @Autowired - private MockMvc mockMvc; - - @MockBean - private AuthQueryService authQueryService; - - @MockBean - private MailService mailService; - - @Test - void sendMail() throws Exception { - //given - var content = objectMapper.writeValueAsString(MemberFixture.무민_회원가입_요청); - - //when - var 요청 = mockMvc.perform( - post("/mail/authorization-code") - .contentType(MediaType.APPLICATION_JSON) - .content(content)); - - //then - 요청.andExpect(status().isOk()); - } - -} \ No newline at end of file +//package com.integrated.techhub.mail.presentation; +// +//import com.fasterxml.jackson.databind.ObjectMapper; +//import com.integrated.techhub.auth.application.AuthQueryService; +//import com.integrated.techhub.mail.application.MailService; +//import com.integrated.techhub.member.fixture.MemberFixture; +//import org.junit.jupiter.api.DisplayNameGeneration; +//import org.junit.jupiter.api.DisplayNameGenerator; +//import org.junit.jupiter.api.Test; +//import org.junit.jupiter.api.extension.ExtendWith; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +//import org.springframework.boot.test.mock.mockito.MockBean; +//import org.springframework.http.MediaType; +//import org.springframework.test.context.junit.jupiter.SpringExtension; +//import org.springframework.test.web.servlet.MockMvc; +// +//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +//import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +// +// +//@ExtendWith(SpringExtension.class) +//@SuppressWarnings("NonAsciiCharacters") +//@WebMvcTest(controllers = MailController.class) +//@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +//class MailControllerMockTest { +// +// @Autowired +// private ObjectMapper objectMapper; +// +// @Autowired +// private MockMvc mockMvc; +// +// @MockBean +// private AuthQueryService authQueryService; +// +// @MockBean +// private MailService mailService; +// +// @Test +// void sendMail() throws Exception { +// //given +// var content = objectMapper.writeValueAsString(MemberFixture.무민_회원가입_요청); +// +// //when +// var 요청 = mockMvc.perform( +// post("/mail/authorization-code") +// .contentType(MediaType.APPLICATION_JSON) +// .content(content)); +// +// //then +// 요청.andExpect(status().isOk()); +// } +// +//} \ No newline at end of file diff --git a/src/test/java/com/integrated/techhub/member/domain/AuthorityCodeTest.java b/src/test/java/com/integrated/techhub/member/domain/AuthorityCodeTest.java index edc4c5e..9f95328 100644 --- a/src/test/java/com/integrated/techhub/member/domain/AuthorityCodeTest.java +++ b/src/test/java/com/integrated/techhub/member/domain/AuthorityCodeTest.java @@ -3,7 +3,7 @@ import com.integrated.techhub.mail.domain.AuthorityCode; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; class AuthorityCodeTest { diff --git a/src/test/java/com/integrated/techhub/member/domain/MemberTest.java b/src/test/java/com/integrated/techhub/member/domain/MemberTest.java index f4b3ccd..5375571 100644 --- a/src/test/java/com/integrated/techhub/member/domain/MemberTest.java +++ b/src/test/java/com/integrated/techhub/member/domain/MemberTest.java @@ -1,6 +1,6 @@ package com.integrated.techhub.member.domain; -import com.integrated.techhub.auth.infra.encode.BCryptPasswordEncoder; +import com.integrated.techhub.common.auth.encode.BCryptPasswordEncoder; import com.integrated.techhub.member.fixture.MemberFixture; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/integrated/techhub/member/fixture/MemberFixture.java b/src/test/java/com/integrated/techhub/member/fixture/MemberFixture.java index fdac7ef..cd5c8ac 100644 --- a/src/test/java/com/integrated/techhub/member/fixture/MemberFixture.java +++ b/src/test/java/com/integrated/techhub/member/fixture/MemberFixture.java @@ -1,6 +1,6 @@ package com.integrated.techhub.member.fixture; -import com.integrated.techhub.auth.dto.SignUpRequest; +import com.integrated.techhub.auth.dto.request.SignUpRequest; import com.integrated.techhub.member.domain.Member; import com.integrated.techhub.member.domain.Position; diff --git a/src/test/java/com/integrated/techhub/pr/application/PullRequestQueryServiceTest.java b/src/test/java/com/integrated/techhub/pr/application/PullRequestQueryServiceTest.java deleted file mode 100644 index cad37e9..0000000 --- a/src/test/java/com/integrated/techhub/pr/application/PullRequestQueryServiceTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.integrated.techhub.pr.application; - -import com.integrated.techhub.pr.domain.PullRequest; -import com.integrated.techhub.pr.domain.repository.PullRequestRepository; -import com.integrated.techhub.pr.dto.response.GetPullRequestResponse; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import java.util.List; - -import static com.integrated.techhub.pr.domain.fixture.PullRequestFixture.풀_리퀘스트_생성; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; - -@SpringBootTest -class PullRequestQueryServiceTest { - - @Autowired - private PullRequestRepository pullRequestRepository; - - @Autowired - private PullRequestQueryService pullRequestQueryService; - - @Test - void 멤버_아이디로_풀_리퀘스트를_조회한다() { - // given - PullRequest pullRequest = 풀_리퀘스트_생성(1L, 1L); - pullRequestRepository.save(pullRequest); - - // when - List pullRequests = pullRequestQueryService.getPullRequestByMemberId(1L); - - // then - Assertions.assertAll( - () -> assertThat(pullRequests).hasSize(1), - () -> assertThat(pullRequests.get(0).id()).isEqualTo(1) - ); - } - -} \ No newline at end of file diff --git a/src/test/java/com/integrated/techhub/pr/domain/fixture/PullRequestFixture.java b/src/test/java/com/integrated/techhub/pr/domain/fixture/PullRequestFixture.java deleted file mode 100644 index ab59ff6..0000000 --- a/src/test/java/com/integrated/techhub/pr/domain/fixture/PullRequestFixture.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.integrated.techhub.pr.domain.fixture; - -import com.integrated.techhub.pr.domain.PullRequest; -import com.integrated.techhub.pr.domain.type.Status; - -public class PullRequestFixture { - - public static PullRequest 풀_리퀘스트_생성(Long memberId, Long missionId) { - return PullRequest.builder() - .memberId(memberId) - .missionId(missionId) - .title("[MVC 구현하기 - 2단계] 베베(최원용) 미션 제출합니다.") - .status(Status.OPEN) - .build(); - } - -} diff --git a/src/test/java/com/integrated/techhub/pr/domain/repository/PullRequestRepositoryTest.java b/src/test/java/com/integrated/techhub/pr/domain/repository/PullRequestRepositoryTest.java deleted file mode 100644 index 12f1f9a..0000000 --- a/src/test/java/com/integrated/techhub/pr/domain/repository/PullRequestRepositoryTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.integrated.techhub.pr.domain.repository; - -import com.integrated.techhub.pr.domain.PullRequest; -import com.integrated.techhub.pr.domain.fixture.PullRequestFixture; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; - -import java.util.List; - -import static com.integrated.techhub.pr.domain.fixture.PullRequestFixture.풀_리퀘스트_생성; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; - -@DataJpaTest -class PullRequestRepositoryTest { - - @Autowired - private PullRequestRepository pullRequestRepository; - - @Test - void 멤버_아이디와_일치하는_풀_리퀘스트를_모두_조회한다() { - // given - // TODO: Member, Mission 도메인 만들어지면 memberId, missionId 동적 할당 - PullRequest pullRequest = 풀_리퀘스트_생성(1L, 1L); - pullRequestRepository.save(pullRequest); - - // when - List pullRequests = pullRequestRepository.findByMemberId(1L); - - // then - Assertions.assertAll( - () -> assertThat(pullRequests).hasSize(1), - () -> assertThat(pullRequests.get(0).getMissionId()).isEqualTo(1) - ); - } - -} \ No newline at end of file diff --git a/src/test/java/com/integrated/techhub/pr/presentation/PullRequestControllerSteps.java b/src/test/java/com/integrated/techhub/pr/presentation/PullRequestControllerSteps.java deleted file mode 100644 index 02d116b..0000000 --- a/src/test/java/com/integrated/techhub/pr/presentation/PullRequestControllerSteps.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.integrated.techhub.pr.presentation; - -import com.integrated.techhub.pr.domain.PullRequest; -import com.integrated.techhub.pr.dto.response.GetPullRequestResponse; -import io.restassured.response.ExtractableResponse; -import io.restassured.response.Response; - -import java.util.List; - -import static com.integrated.techhub.common.acceptance.AcceptanceSteps.given; - -public class PullRequestControllerSteps { - - public static List 예상_조회_결과(List 예상_풀_리퀘스트들) { - return 예상_풀_리퀘스트들.stream() - .map(GetPullRequestResponse::of) - .toList(); - } - - public static ExtractableResponse 멤버_아이디로_풀_리퀘스트_조회_요청(Long memberId) { - return given() - .when() - .get("/pull-requests/" + memberId) - .then() - .extract(); - } - -} diff --git a/src/test/java/com/integrated/techhub/pr/presentation/PullRequestControllerTest.java b/src/test/java/com/integrated/techhub/pr/presentation/PullRequestControllerTest.java deleted file mode 100644 index e8fb456..0000000 --- a/src/test/java/com/integrated/techhub/pr/presentation/PullRequestControllerTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.integrated.techhub.pr.presentation; - -import com.integrated.techhub.common.acceptance.AcceptanceTest; -import com.integrated.techhub.pr.domain.PullRequest; -import com.integrated.techhub.pr.domain.repository.PullRequestRepository; -import com.integrated.techhub.pr.domain.type.Status; -import com.integrated.techhub.pr.dto.response.GetPullRequestResponse; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; - -import java.util.ArrayList; -import java.util.List; - -import static com.integrated.techhub.pr.presentation.PullRequestControllerSteps.예상_조회_결과; - -@DisplayName("풀 리퀘스트 인수 테스트") -class PullRequestControllerTest extends AcceptanceTest { - - @Autowired - private PullRequestRepository pullRequestRepository; - - @Test - void 풀_리퀘스트를_멤버_아이디로_조회한다() { - // given - List 저장한_풀_리퀘스트들 = 풀_리퀘스트들을_저장한다(); - List 예상_조회_결과 = 예상_조회_결과(List.of(저장한_풀_리퀘스트들.get(0))); - - // when - var 응답 = PullRequestControllerSteps.멤버_아이디로_풀_리퀘스트_조회_요청(저장한_풀_리퀘스트들.get(0).getId()); - - // then - 응답.response().then().statusCode(HttpStatus.OK.value()); - } - - private List 풀_리퀘스트들을_저장한다() { - ArrayList pullRequests = new ArrayList<>(); - for (Long i = 1L; i <= 10L; i++) { - PullRequest pullRequest = PullRequest.builder() - .memberId(i) - .missionId(i) - .title("[MVC 구현하기 - 2단계] 베베(최원용) 미션 제출합니다.") - .status(Status.OPEN) - .build(); - pullRequests.add(pullRequest); - } - pullRequestRepository.saveAll(pullRequests); - return pullRequests; - } - -} \ No newline at end of file