diff --git a/.github/workflows/backend_pr_decorator.yml b/.github/workflows/backend_pr_decorator.yml index 92236bdf0..246e81b8d 100644 --- a/.github/workflows/backend_pr_decorator.yml +++ b/.github/workflows/backend_pr_decorator.yml @@ -36,7 +36,7 @@ jobs: - name: run jacocoTestCoverage run: | cd backend/ddang - ./gradlew jacocoTestCoverage --info + ./gradlew jacocoTestCoverage - name: set author slack id if: always() @@ -50,9 +50,9 @@ jobs: elif [ "$GIT_ID" == "swonny" ]; then AUTHOR_NAME="${{ secrets.swonny_slack_display_name }}" AUTHOR_ID="${{ secrets.swonny_slack_id }}" - elif [ "$GIT_ID" == "jj503" ]; then - AUTHOR_NAME="${{ secrets.jj503_slack_display_name }}" - AUTHOR_ID="${{ secrets.jj503_slack_id }}" + elif [ "$GIT_ID" == "JJ503" ]; then + AUTHOR_NAME="${{ secrets.JJ503_slack_display_name }}" + AUTHOR_ID="${{ secrets.JJ503_slack_id }}" elif [ "$GIT_ID" == "kwonyj1022" ]; then AUTHOR_NAME="${{ secrets.kwonyj1022_slack_display_name }}" AUTHOR_ID="${{ secrets.kwonyj1022_slack_id }}" @@ -61,44 +61,9 @@ jobs: echo "AUTHOR_NAME=${AUTHOR_NAME}" >> $GITHUB_OUTPUT echo "AUTHOR_ID=${AUTHOR_ID}" >> $GITHUB_OUTPUT - - name: run an analysis of the ${{ github.REF }} branch ${{ github.BASE_REF }} base - uses: sonarsource/sonarqube-scan-action@master - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ secrets.SONAR_URL }} - with: - args: > - -Dsonar.issuesReport.console.enable=true - -Dsonar.projectKey=develop-be-project - -Dsonar.java.binaries=backend/ddang/build/classes - -Dsonar.exclusions=**/*Dto*.java,**/*Application*.java,**/*Exception*.java,**/*Response*.java,**/*Request*.java,**/*Configuration*.java,**/*Appender*.java,**/*.html,**/generated/**,**/resources/** - -Dsonar.sourceEncoding=UTF-8 - -Dsonar.java.coveragePlugin=jacoco - -Dsonar.coverage.jacoco.xmlReportPaths=backend/ddang/build/reports/jacoco/test/jacocoTestReport.xml - -Dsonar.issue.ignore.multicriteria=e1,e2 - -Dsonar.issue.ignore.multicriteria.e1.ruleKey=java:S100 - -Dsonar.issue.ignore.multicriteria.e1.resourceKey=**/*Test.java - -Dsonar.issue.ignore.multicriteria.e2.ruleKey=java:S1192 - -Dsonar.issue.ignore.multicriteria.e2.resourceKey=**/*Test.java - - - name: sonarqube quality check - id: sonar-quality - run: | - SONAR_PROJECT_KEY="develop-be-project" - SONAR_TOKEN="${{ secrets.SONAR_TOKEN }}" - - RESULT=$(curl -s -u "admin:root" "${{ secrets.SONAR_URL }}/api/qualitygates/project_status?projectKey=${SONAR_PROJECT_KEY}&pullRequest=${{github.event.number}}") - STATUS=$(echo "$RESULT" | jq -r '.projectStatus.status') - ERROR_METRIC_KEYS=$(echo "$RESULT" | jq -r '.projectStatus.conditions[] | select(.status == "ERROR").metricKey') - - echo "STATUS=${STATUS}" >> $GITHUB_OUTPUT - echo "ERROR_METRIC_KEYS=${ERROR_METRIC_KEYS}" >> $GITHUB_OUTPUT - - name: set variables id: variables run: | - SONAR_SCANNER_URL="${{ secrets.SONAR_URL }}/dashboard?id=sonarqube-test&pullRequest=${{github.event.number}}" - REVIEWERS_GIT_ID='${{ toJson(github.event.pull_request.requested_reviewers[*].login) }}' reviewers=$(echo "$REVIEWERS_GIT_ID" | jq -r '.[]') @@ -111,32 +76,34 @@ jobs: elif [ "$reviewer" == "swonny" ]; then REVIEWERS_SLACK_ID+="<@${{ secrets.swonny_slack_id }}> " elif [ "$reviewer" == "JJ503" ]; then - REVIEWERS_SLACK_ID+="<@${{ secrets.jj503_slack_id }}> " + REVIEWERS_SLACK_ID+="<@${{ secrets.JJ503_slack_id }}> " elif [ "$reviewer" == "kwonyj1022" ]; then REVIEWERS_SLACK_ID+="<@${{ secrets.kwonyj1022_slack_id }}> " fi done echo "AUTHOR=${AUTHOR}" >> $GITHUB_OUTPUT - echo "SONAR_SCANNER_URL=${SONAR_SCANNER_URL}" >> $GITHUB_OUTPUT echo "REVIEWERS=${REVIEWERS}" >> $GITHUB_OUTPUT echo "REVIEWERS_SLACK_ID=${REVIEWERS_SLACK_ID}" >> $GITHUB_OUTPUT - - name: slack test + - name: slack notification + if: github.event_name == 'pull_request' && github.event.action != 'synchronize' run: | SLACK_MESSAGE='{"text":"PR 브랜치 분석","blocks":[{"type":"section","text":{"type":"mrkdwn","text":">*PR 브랜치 분석* \n>\n>*PR Author*\n>' SLACK_MESSAGE+="${{ steps.author-slack.outputs.AUTHOR_NAME }}" SLACK_MESSAGE+="\n>\n>*PR 링크*\n><" SLACK_MESSAGE+="${{ github.event.pull_request.html_url }} " - SLACK_MESSAGE+="> \n>\n>분석 결과\n>" + SLACK_MESSAGE+="> \n>\n>*PR 제목*\n>" + SLACK_MESSAGE+="${{ github.event.pull_request.title }}" + SLACK_MESSAGE+="\n>\n>분석 결과\n>" SLACK_MESSAGE+=":white_check_mark:" SLACK_MESSAGE+="\n>\n>*리뷰어*\n>" SLACK_MESSAGE+="${{ steps.variables.outputs.REVIEWERS_SLACK_ID }}" SLACK_MESSAGE+='"}}]}' - curl -X POST ${{ secrets.SLACK_WEBHOOK }} -d "${SLACK_MESSAGE}" + curl -X POST ${{ secrets.SLACK_WEBHOOK }} -d "${SLACK_MESSAGE}" - - name: slack failed test + - name: slack failed notification if: failure() run: | SLACK_MESSAGE='{"text":"PR 브랜치 분석","blocks":[{"type":"section","text":{"type":"mrkdwn","text":">*PR 브랜치 분석* \n>\n>*PR Author*\n>' @@ -145,13 +112,15 @@ jobs: SLACK_MESSAGE+=">" SLACK_MESSAGE+="\n>\n>*PR 링크*\n><" SLACK_MESSAGE+="${{ github.event.pull_request.html_url }} " - SLACK_MESSAGE+="> \n>\n>분석 결과\n>" + SLACK_MESSAGE+="> \n>\n>*PR 제목*\n>" + SLACK_MESSAGE+="${{ github.event.pull_request.title }}" + SLACK_MESSAGE+="\n>\n>분석 결과\n>" SLACK_MESSAGE+=":x:" SLACK_MESSAGE+='"}}]}' curl -X POST ${{ secrets.SLACK_WEBHOOK }} -d "${SLACK_MESSAGE}" - - name: slack cancelled test + - name: slack cancelled notification if: cancelled() run: | SLACK_MESSAGE='{"text":"PR 브랜치 분석","blocks":[{"type":"section","text":{"type":"mrkdwn","text":">*PR 브랜치 분석* \n>\n>*PR Author*\n>' @@ -160,7 +129,9 @@ jobs: SLACK_MESSAGE+=">" SLACK_MESSAGE+="\n>\n>*PR 링크*\n><" SLACK_MESSAGE+="${{ github.event.pull_request.html_url }} " - SLACK_MESSAGE+="> \n>\n>분석 결과\n>" + SLACK_MESSAGE+="> \n>\n>*PR 제목*\n>" + SLACK_MESSAGE+="${{ github.event.pull_request.title }}" + SLACK_MESSAGE+="\n>\n>분석 결과\n>" SLACK_MESSAGE+=":black_square_for_stop:" SLACK_MESSAGE+='"}}]}' diff --git a/backend/ddang/.gitignore b/backend/ddang/.gitignore index 6f47e9bda..7a65959ae 100644 --- a/backend/ddang/.gitignore +++ b/backend/ddang/.gitignore @@ -178,3 +178,4 @@ Temporary Items *.icloud # End of https://www.toptal.com/developers/gitignore/api/macos,java,intellij +/src/main/resources/firebase/ diff --git a/backend/ddang/build.gradle b/backend/ddang/build.gradle index 1e5dfe26d..ccbc8a1e9 100644 --- a/backend/ddang/build.gradle +++ b/backend/ddang/build.gradle @@ -3,7 +3,7 @@ plugins { id 'org.springframework.boot' version '3.0.8' id 'io.spring.dependency-management' version '1.1.0' id 'jacoco' - id "org.asciidoctor.jvm.convert" version "3.3.2" + id 'org.asciidoctor.jvm.convert' version '3.3.2' } configurations { @@ -28,28 +28,51 @@ repositories { } 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' - implementation 'org.flywaydb:flyway-core' - implementation 'org.flywaydb:flyway-mysql:9.16.0' - compileOnly 'org.projectlombok:lombok' + // h2 runtimeOnly 'com.h2database:h2' + + // mysql + runtimeOnly 'com.mysql:mysql-connector-j' + + // lombok + compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + + // flyway + implementation 'org.flywaydb:flyway-core' + implementation 'org.flywaydb:flyway-mysql:9.16.0' + + // rest docs testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' + + // jpa + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" - annotationProcessor "jakarta.annotation:jakarta.annotation-api" - annotationProcessor "jakarta.persistence:jakarta.persistence-api" - runtimeOnly 'com.mysql:mysql-connector-j' + + // jwt implementation 'io.jsonwebtoken:jjwt-api:0.11.2' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2', 'io.jsonwebtoken:jjwt-jackson:0.11.2' - annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2' + + // monitoring implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' + // fcm + implementation 'com.google.firebase:firebase-admin:9.2.0' + + // logging + implementation 'ch.qos.logback.contrib:logback-json-classic:0.1.5' + implementation 'net.logstash.logback:logstash-logback-encoder:6.1' + + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + annotationProcessor 'jakarta.annotation:jakarta.annotation-api' + annotationProcessor 'jakarta.persistence:jakarta.persistence-api' + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' } tasks.withType(JavaCompile) { @@ -77,8 +100,8 @@ jacocoTestReport { def Qdomains = [] - for(qPattern in "**/QA" .. "**/QZ"){ - Qdomains.add(qPattern+"*") + for(qPattern in '**/QA' .. '**/QZ'){ + Qdomains.add(qPattern + '*') } afterEvaluate { diff --git a/backend/ddang/script/dev-pipeline.jenkinsfile b/backend/ddang/script/dev-pipeline.jenkinsfile index 7f5b187b1..4190084e7 100644 --- a/backend/ddang/script/dev-pipeline.jenkinsfile +++ b/backend/ddang/script/dev-pipeline.jenkinsfile @@ -1,21 +1,13 @@ pipeline { agent any - tools { - jdk "jdk-17.0.7" - } - - environment { - JAVA_HOME = "tool jdk-17.0.7" - } - stages { stage('build start notification to slack') { steps { slackSend ( channel: '#빌드-결과', color: '#FFFF00', - message: "빌드 시작!: pipeline : ${env.JOB_NAME}#${env.BUILD_NUMBER} (http://${env.EC2_PUBLIC_IP}/jenkins)" + message: "빌드 시작!: pipeline : ${env.JOB_NAME}#${env.BUILD_NUMBER} (http://${env.INFRA_EC2_PUBLIC_IP}/jenkins)" ) } } @@ -25,7 +17,7 @@ pipeline { checkout scmGit( branches: [[name: '*/develop-be']], extensions: [submodule(parentCredentials: true, reference: '', trackingSubmodules: true)], - userRemoteConfigs: [[credentialsId: 'ddang-dev-token', url: 'https://github.com/woowacourse-teams/2023-3-ddang']]) + userRemoteConfigs: [[credentialsId: 'github-token', url: 'https://github.com/woowacourse-teams/2023-3-ddang']]) } } @@ -40,11 +32,11 @@ pipeline { stage('publish') { steps { - sshagent(credentials: ['ddang-dev-ec2']) { - sh "scp backend/ddang/build/libs/ddang-0.0.1-SNAPSHOT.jar ubuntu@${env.EC2_PRIVATE_IP}:${env.EC2_DIRECTORY}" - sh "scp backend/ddang/3-ddang-submodule/application-dev.yml ubuntu@${env.EC2_PRIVATE_IP}:${env.EC2_DIRECTORY}" - sh "scp backend/ddang/script/dev-deploy-script.sh ubuntu@${env.EC2_PRIVATE_IP}:${env.EC2_DIRECTORY}" - sh "ssh ubuntu@${env.EC2_PRIVATE_IP} 'sh ${env.EC2_DIRECTORY}/dev-deploy-script.sh' " + sshagent(credentials: ['ec2-dev-key']) { + sh "scp backend/ddang/build/libs/ddang-0.0.1-SNAPSHOT.jar ubuntu@${env.DEV_EC2_PRIVATE_IP}:${env.EC2_DIRECTORY}" + sh "scp backend/ddang/3-ddang-submodule/application-dev.yml ubuntu@${env.DEV_EC2_PRIVATE_IP}:${env.EC2_DIRECTORY}" + sh "scp backend/ddang/script/dev-deploy-script.sh ubuntu@${env.DEV_EC2_PRIVATE_IP}:${env.EC2_DIRECTORY}" + sh "ssh ubuntu@${env.DEV_EC2_PRIVATE_IP} 'sh ${env.EC2_DIRECTORY}/dev-deploy-script.sh' " } } } @@ -62,7 +54,7 @@ pipeline { channel: '#빌드-결과', color: '#00FF00', message: """ - 빌드 성공!: pipeline : ${env.JOB_NAME}#${env.BUILD_NUMBER} (http://${env.EC2_PUBLIC_IP}/jenkins) + 빌드 성공!: pipeline : ${env.JOB_NAME}#${env.BUILD_NUMBER} (http://${env.INFRA_EC2_PUBLIC_IP}/jenkins) """ ) } @@ -70,7 +62,7 @@ pipeline { slackSend ( channel: '#빌드-결과', color: '#FF0000', - message: "\n빌드 실패...: pipeline : ${env.JOB_NAME}#${env.BUILD_NUMBER} (http://${env.EC2_PUBLIC_IP}/jenkins)" + message: "\n빌드 실패...: pipeline : ${env.JOB_NAME}#${env.BUILD_NUMBER} (http://${env.INFRA_EC2_PUBLIC_IP}/jenkins)" ) } } diff --git a/backend/ddang/script/prod-pipeline.jenkinsfile b/backend/ddang/script/prod-pipeline.jenkinsfile index 74933c53c..50fa57a45 100644 --- a/backend/ddang/script/prod-pipeline.jenkinsfile +++ b/backend/ddang/script/prod-pipeline.jenkinsfile @@ -1,21 +1,13 @@ pipeline { agent any - tools { - jdk "jdk-17.0.7" - } - - environment { - JAVA_HOME = "tool jdk-17.0.7" - } - stages { stage('build start notification to slack') { steps { slackSend ( channel: '#빌드-결과', color: '#FFFF00', - message: "빌드 시작!: pipeline : ${env.JOB_NAME}#${env.BUILD_NUMBER} (http://${env.EC2_PUBLIC_IP}/jenkins)" + message: "빌드 시작!: pipeline : ${env.JOB_NAME}#${env.BUILD_NUMBER} (http://${env.INFRA_EC2_PUBLIC_IP}/jenkins)" ) } } @@ -25,7 +17,7 @@ pipeline { checkout scmGit( branches: [[name: '*/develop']], extensions: [submodule(parentCredentials: true, reference: '', trackingSubmodules: true)], - userRemoteConfigs: [[credentialsId: 'ddang-dev-token', url: 'https://github.com/woowacourse-teams/2023-3-ddang']]) + userRemoteConfigs: [[credentialsId: 'github-token', url: 'https://github.com/woowacourse-teams/2023-3-ddang']]) } } @@ -40,7 +32,7 @@ pipeline { stage('publish') { steps { - sshagent(credentials: ['ddang-prod-ec2']) { + sshagent(credentials: ['ec2-prod-key']) { sh "scp backend/ddang/build/libs/ddang-0.0.1-SNAPSHOT.jar ubuntu@${env.PROD_EC2_PRIVATE_IP}:${env.EC2_DIRECTORY}" sh "scp backend/ddang/3-ddang-submodule/application-prod.yml ubuntu@${env.PROD_EC2_PRIVATE_IP}:${env.EC2_DIRECTORY}" sh "scp backend/ddang/script/prod-deploy-script.sh ubuntu@${env.PROD_EC2_PRIVATE_IP}:${env.EC2_DIRECTORY}" @@ -62,7 +54,7 @@ pipeline { channel: '#빌드-결과', color: '#00FF00', message: """ - 빌드 성공!: pipeline : ${env.JOB_NAME}#${env.BUILD_NUMBER} (http://${env.EC2_PUBLIC_IP}/jenkins) + 빌드 성공!: pipeline : ${env.JOB_NAME}#${env.BUILD_NUMBER} (http://${env.INFRA_EC2_PUBLIC_IP}/jenkins) """ ) } @@ -70,7 +62,7 @@ pipeline { slackSend ( channel: '#빌드-결과', color: '#FF0000', - message: "\n빌드 실패...: pipeline : ${env.JOB_NAME}#${env.BUILD_NUMBER} (http://${env.EC2_PUBLIC_IP}/jenkins)" + message: "\n빌드 실패...: pipeline : ${env.JOB_NAME}#${env.BUILD_NUMBER} (http://${env.INFRA_EC2_PUBLIC_IP}/jenkins)" ) } } diff --git a/backend/ddang/src/docs/asciidoc/docs.adoc b/backend/ddang/src/docs/asciidoc/docs.adoc index e0c99fb1e..0e0a8549b 100644 --- a/backend/ddang/src/docs/asciidoc/docs.adoc +++ b/backend/ddang/src/docs/asciidoc/docs.adoc @@ -75,6 +75,42 @@ include::{snippets}/authentication-controller-test/access-token과_refresh-token include::{snippets}/authentication-controller-test/access-token과_refresh-token을_전달하면_로그아웃한다/http-response.adoc[] +=== 탈퇴 + +==== 요청 + +include::{snippets}/authentication-controller-test/ouath2-type과_access-token과_refresh-token을_전달하면_탈퇴한다/http-request.adoc[] +include::{snippets}/authentication-controller-test/ouath2-type과_access-token과_refresh-token을_전달하면_탈퇴한다/path-parameters.adoc[] +include::{snippets}/authentication-controller-test/ouath2-type과_access-token과_refresh-token을_전달하면_탈퇴한다/request-headers.adoc[] +include::{snippets}/authentication-controller-test/ouath2-type과_access-token과_refresh-token을_전달하면_탈퇴한다/request-fields.adoc[] + +==== 응답 + +include::{snippets}/authentication-controller-test/ouath2-type과_access-token과_refresh-token을_전달하면_탈퇴한다/http-response.adoc[] + +== 사용자 정보 API +=== 사용자 정보 조회 + +==== 요청 + +include::{snippets}/user-controller-test/사용자_정보를_조회한다/http-request.adoc[] + +==== 응답 + +include::{snippets}/user-controller-test/사용자_정보를_조회한다/http-response.adoc[] +include::{snippets}/user-controller-test/사용자_정보를_조회한다/response-fields.adoc[] + +=== 사용자 정보 수정 + +==== 요청 + +include::{snippets}/user-controller-test/사용자_정보를_모두_수정한다/http-request.adoc[] + +==== 응답 + +include::{snippets}/user-controller-test/사용자_정보를_모두_수정한다/http-response.adoc[] +include::{snippets}/user-controller-test/사용자_정보를_모두_수정한다/response-fields.adoc[] + == 카테고리 API === 메인 카테고리 조회 @@ -157,18 +193,46 @@ include::{snippets}/auction-controller-test/경매를_등록한다/response-fiel include::{snippets}/auction-controller-test/첫번째_페이지의_경매_목록을_조회한다/http-request.adoc[] include::{snippets}/auction-controller-test/첫번째_페이지의_경매_목록을_조회한다/query-parameters.adoc[] +include::{snippets}/auction-controller-test/첫번째_페이지의_경매_목록을_조회한다/request-headers.adoc[] ==== 응답 include::{snippets}/auction-controller-test/첫번째_페이지의_경매_목록을_조회한다/http-response.adoc[] include::{snippets}/auction-controller-test/첫번째_페이지의_경매_목록을_조회한다/response-fields.adoc[] +=== 자신이 등록한 경매 목록 조회 + +==== 요청 + +include::{snippets}/user-auction-controller-test/로그인한_회원이_등록한_경매_목록을_조회한다/http-request.adoc[] +include::{snippets}/user-auction-controller-test/로그인한_회원이_등록한_경매_목록을_조회한다/query-parameters.adoc[] +include::{snippets}/user-auction-controller-test/로그인한_회원이_등록한_경매_목록을_조회한다/request-headers.adoc[] + +==== 응답 + +include::{snippets}/user-auction-controller-test/로그인한_회원이_등록한_경매_목록을_조회한다/http-response.adoc[] +include::{snippets}/user-auction-controller-test/로그인한_회원이_등록한_경매_목록을_조회한다/response-fields.adoc[] + +=== 자신이 참여한 경매 목록 조회 + +==== 요청 + +include::{snippets}/user-auction-controller-test/로그인한_회원이_참여한_경매_목록을_조회한다/http-request.adoc[] +include::{snippets}/user-auction-controller-test/로그인한_회원이_참여한_경매_목록을_조회한다/query-parameters.adoc[] +include::{snippets}/user-auction-controller-test/로그인한_회원이_참여한_경매_목록을_조회한다/request-headers.adoc[] + +==== 응답 + +include::{snippets}/user-auction-controller-test/로그인한_회원이_참여한_경매_목록을_조회한다/http-response.adoc[] +include::{snippets}/user-auction-controller-test/로그인한_회원이_참여한_경매_목록을_조회한다/response-fields.adoc[] + === 경매 상세 조회 ==== 요청 include::{snippets}/auction-controller-test/지정한_아이디에_해당하는_경매를_조회한다/http-request.adoc[] include::{snippets}/auction-controller-test/지정한_아이디에_해당하는_경매를_조회한다/path-parameters.adoc[] +include::{snippets}/auction-controller-test/지정한_아이디에_해당하는_경매를_조회한다/request-headers.adoc[] ==== 응답 @@ -187,6 +251,67 @@ include::{snippets}/auction-controller-test/지정한_아이디에_해당하는_ include::{snippets}/auction-controller-test/지정한_아이디에_해당하는_경매를_삭제한다/http-response.adoc[] +== Q&A API + +=== 질문 등록 + +==== 요청 + +include::{snippets}/qna-controller-test/질문을_등록한다/http-request.adoc[] +include::{snippets}/qna-controller-test/질문을_등록한다/request-headers.adoc[] +include::{snippets}/qna-controller-test/질문을_등록한다/request-fields.adoc[] + +==== 응답 + +include::{snippets}/qna-controller-test/질문을_등록한다/http-response.adoc[] + +=== 질문 삭제 + +==== 요청 + +include::{snippets}/qna-controller-test/질문을_삭제한다/http-request.adoc[] +include::{snippets}/qna-controller-test/질문을_삭제한다/request-headers.adoc[] +include::{snippets}/qna-controller-test/질문을_삭제한다/path-parameters.adoc[] + +==== 응답 + +include::{snippets}/qna-controller-test/질문을_삭제한다/http-response.adoc[] + +=== 답변 등록 + +==== 요청 + +include::{snippets}/qna-controller-test/답변을_등록한다/http-request.adoc[] +include::{snippets}/qna-controller-test/답변을_등록한다/request-headers.adoc[] +include::{snippets}/qna-controller-test/답변을_등록한다/request-fields.adoc[] + +==== 응답 + +include::{snippets}/qna-controller-test/답변을_등록한다/http-response.adoc[] + +=== 답변 삭제 + +==== 요청 + +include::{snippets}/qna-controller-test/답변을_삭제한다/http-request.adoc[] +include::{snippets}/qna-controller-test/답변을_삭제한다/request-headers.adoc[] +include::{snippets}/qna-controller-test/답변을_삭제한다/path-parameters.adoc[] + +==== 응답 + +include::{snippets}/qna-controller-test/답변을_삭제한다/http-response.adoc[] + +=== Q&A 조회 + +==== 요청 + +include::{snippets}/auction-qna-controller-test/경매_아이디를_통해_질문과_답변을_모두_조회한다/http-request.adoc[] + +==== 응답 + +include::{snippets}/auction-qna-controller-test/경매_아이디를_통해_질문과_답변을_모두_조회한다/http-response.adoc[] +include::{snippets}/auction-qna-controller-test/경매_아이디를_통해_질문과_답변을_모두_조회한다/response-fields.adoc[] + == 입찰 API === 입찰 등록 @@ -326,3 +451,111 @@ include::{snippets}/report-controller-test/전체_채팅방_신고_목록을_조 include::{snippets}/report-controller-test/전체_채팅방_신고_목록을_조회한다/http-response.adoc[] include::{snippets}/report-controller-test/전체_채팅방_신고_목록을_조회한다/response-fields.adoc[] + +=== 질문 신고 등록 + +==== 요청 + +include::{snippets}/report-controller-test/질문_신고를_등록한다/http-request.adoc[] +include::{snippets}/report-controller-test/질문_신고를_등록한다/request-headers.adoc[] +include::{snippets}/report-controller-test/질문_신고를_등록한다/request-fields.adoc[] + +==== 응답 + +include::{snippets}/report-controller-test/질문_신고를_등록한다/http-response.adoc[] + +=== 채팅방 신고 조회 + +==== 요청 + +include::{snippets}/report-controller-test/전체_질문_신고_목록을_조회한다/http-request.adoc[] + +==== 응답 + +include::{snippets}/report-controller-test/전체_질문_신고_목록을_조회한다/http-response.adoc[] +include::{snippets}/report-controller-test/전체_질문_신고_목록을_조회한다/response-fields.adoc[] + +=== 답변 신고 등록 + +==== 요청 + +include::{snippets}/report-controller-test/답변_신고를_등록한다/http-request.adoc[] +include::{snippets}/report-controller-test/답변_신고를_등록한다/request-headers.adoc[] +include::{snippets}/report-controller-test/답변_신고를_등록한다/request-fields.adoc[] + +==== 응답 + +include::{snippets}/report-controller-test/답변_신고를_등록한다/http-response.adoc[] + +=== 채팅방 신고 조회 + +==== 요청 + +include::{snippets}/report-controller-test/전체_답변_신고_목록을_조회한다/http-request.adoc[] + +==== 응답 + +include::{snippets}/report-controller-test/전체_답변_신고_목록을_조회한다/http-response.adoc[] +include::{snippets}/report-controller-test/전체_답변_신고_목록을_조회한다/response-fields.adoc[] + +== 디바이스 토큰 API + +=== 디바이스 토큰 갱신 + +==== 요청 + +include::{snippets}/device-token-controller-test/디바이스_토큰을_저장_또는_갱신한다/http-request.adoc[] +include::{snippets}/device-token-controller-test/디바이스_토큰을_저장_또는_갱신한다/request-headers.adoc[] +include::{snippets}/device-token-controller-test/디바이스_토큰을_저장_또는_갱신한다/request-fields.adoc[] + +==== 응답 + +include::{snippets}/device-token-controller-test/디바이스_토큰을_저장_또는_갱신한다/http-response.adoc[] + +== 유저 상호 평가 API + +=== 사용자 평가 등록 + +==== 요청 + +include::{snippets}/review-controller-test/평가를_등록한다/http-request.adoc[] +include::{snippets}/review-controller-test/평가를_등록한다/request-headers.adoc[] +include::{snippets}/review-controller-test/평가를_등록한다/request-fields.adoc[] + +==== 응답 + +include::{snippets}/review-controller-test/평가를_등록한다/http-response.adoc[] + +=== 지정한 평가 아이디에 해당하는 평가 조회 + +==== 요청 + +include::{snippets}/review-controller-test/지정한_평가_아이디에_해당하는_평가를_조회한다/http-request.adoc[] +include::{snippets}/review-controller-test/지정한_평가_아이디에_해당하는_평가를_조회한다/path-parameters.adoc[] + +==== 응답 +include::{snippets}/review-controller-test/지정한_평가_아이디에_해당하는_평가를_조회한다/http-response.adoc[] +include::{snippets}/review-controller-test/지정한_평가_아이디에_해당하는_평가를_조회한다/response-fields.adoc[] + +=== 지정한 사용자가 받은 평가 목록 조회 + +==== 요청 + +include::{snippets}/review-controller-test/주어진_사용자가_받은_평가_목록을_최신순으로_조회한다/http-request.adoc[] +include::{snippets}/review-controller-test/주어진_사용자가_받은_평가_목록을_최신순으로_조회한다/path-parameters.adoc[] + +==== 응답 + +include::{snippets}/review-controller-test/주어진_사용자가_받은_평가_목록을_최신순으로_조회한다/http-response.adoc[] +include::{snippets}/review-controller-test/주어진_사용자가_받은_평가_목록을_최신순으로_조회한다/response-fields.adoc[] + +=== 사용자가_경매_거래_상대에게_작성한_평가를_조회한다 + +==== 요청 +include::{snippets}/auction-review-controller-test/사용자가_경매_거래에_작성한_평가를_조회한다/http-request.adoc[] +include::{snippets}/auction-review-controller-test/사용자가_경매_거래에_작성한_평가를_조회한다/path-parameters.adoc[] +include::{snippets}/auction-review-controller-test/사용자가_경매_거래에_작성한_평가를_조회한다/request-headers.adoc[] + +==== 응답 +include::{snippets}/auction-review-controller-test/사용자가_경매_거래에_작성한_평가를_조회한다/http-response.adoc[] +include::{snippets}/auction-review-controller-test/사용자가_경매_거래에_작성한_평가를_조회한다/response-fields.adoc[] diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/application/AuctionService.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/application/AuctionService.java index 1a00d4e05..725365c48 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/application/AuctionService.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/application/AuctionService.java @@ -3,19 +3,15 @@ import com.ddang.ddang.auction.application.dto.CreateAuctionDto; import com.ddang.ddang.auction.application.dto.CreateInfoAuctionDto; import com.ddang.ddang.auction.application.dto.ReadAuctionDto; -import com.ddang.ddang.auction.application.dto.ReadAuctionWithChatRoomIdDto; import com.ddang.ddang.auction.application.dto.ReadAuctionsDto; -import com.ddang.ddang.auction.application.dto.ReadChatRoomDto; import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; import com.ddang.ddang.auction.application.exception.UserForbiddenException; import com.ddang.ddang.auction.domain.Auction; import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; -import com.ddang.ddang.authentication.domain.dto.AuthenticationUserInfo; +import com.ddang.ddang.auction.presentation.dto.request.ReadAuctionSearchCondition; import com.ddang.ddang.category.application.exception.CategoryNotFoundException; import com.ddang.ddang.category.domain.Category; import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; -import com.ddang.ddang.chat.domain.ChatRoom; -import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; import com.ddang.ddang.image.domain.AuctionImage; import com.ddang.ddang.image.domain.StoreImageProcessor; import com.ddang.ddang.image.domain.dto.StoreImageDto; @@ -26,24 +22,20 @@ import com.ddang.ddang.user.application.exception.UserNotFoundException; import com.ddang.ddang.user.domain.User; import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import java.time.LocalDateTime; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - @Service @Transactional(readOnly = true) @RequiredArgsConstructor public class AuctionService { - public static final Long DEFAULT_CHAT_ROOM_ID = null; - private final JpaUserRepository userRepository; - private final JpaChatRoomRepository chatRoomRepository; private final JpaAuctionRepository auctionRepository; private final JpaRegionRepository regionRepository; private final JpaCategoryRepository categoryRepository; @@ -51,11 +43,26 @@ public class AuctionService { @Transactional public CreateInfoAuctionDto create(final CreateAuctionDto dto) { - final User seller = findSeller(dto); - final Category subCategory = findSubCategory(dto); + final User seller = userRepository.findById(dto.sellerId()) + .orElseThrow(() -> new UserNotFoundException( + "지정한 판매자를 찾을 수 없습니다." + )); + final Category subCategory = categoryRepository.findSubCategoryById(dto.subCategoryId()) + .orElseThrow(() -> new CategoryNotFoundException( + "지정한 하위 카테고리가 없거나 하위 카테고리가 아닙니다." + )); final Auction auction = dto.toEntity(seller, subCategory); - final List auctionRegions = convertAuctionRegions(dto); - final List auctionImages = convertAuctionImages(dto); + final List thirdRegions = regionRepository.findAllThirdRegionByIds(dto.thirdRegionIds()); + + validateAuctionRegions(thirdRegions); + + final List auctionRegions = thirdRegions.stream() + .map(AuctionRegion::new) + .toList(); + final List auctionImages = imageProcessor.storeImageFiles(dto.auctionImages()) + .stream() + .map(StoreImageDto::toAuctionImageEntity) + .toList(); auction.addAuctionRegions(auctionRegions); auction.addAuctionImages(auctionImages); @@ -65,64 +72,42 @@ public CreateInfoAuctionDto create(final CreateAuctionDto dto) { return CreateInfoAuctionDto.from(persistAuction); } - private User findSeller(final CreateAuctionDto dto) { - return userRepository.findById(dto.sellerId()) - .orElseThrow(() -> new UserNotFoundException("지정한 판매자를 찾을 수 없습니다.")); - } - - private Category findSubCategory(final CreateAuctionDto dto) { - return categoryRepository.findSubCategoryById(dto.subCategoryId()) - .orElseThrow(() -> new CategoryNotFoundException( - "지정한 하위 카테고리가 없거나 하위 카테고리가 아닙니다." - )); - } - - private List convertAuctionRegions(final CreateAuctionDto dto) { - final List auctionRegions = new ArrayList<>(); - - for (final Long thirdRegionId : dto.thirdRegionIds()) { - final Region thirdRegion = regionRepository.findThirdRegionById(thirdRegionId) - .orElseThrow(() -> new RegionNotFoundException( - "지정한 세 번째 지역이 없거나 세 번째 지역이 아닙니다." - )); - auctionRegions.add(new AuctionRegion(thirdRegion)); + private void validateAuctionRegions(final List thirdRegions) { + if (thirdRegions.isEmpty()) { + throw new RegionNotFoundException("지정한 세 번째 지역이 없습니다."); } - - return auctionRegions; - } - - private List convertAuctionImages(final CreateAuctionDto dto) { - return imageProcessor.storeImageFiles(dto.auctionImages()) - .stream() - .map(StoreImageDto::toEntity) - .toList(); } - public ReadAuctionWithChatRoomIdDto readByAuctionId(final Long auctionId, final AuthenticationUserInfo userInfo) { + public ReadAuctionDto readByAuctionId(final Long auctionId) { final Auction findAuction = auctionRepository.findAuctionById(auctionId) .orElseThrow(() -> new AuctionNotFoundException( "지정한 아이디에 대한 경매를 찾을 수 없습니다." )); - final User findUser = userRepository.findById(userInfo.userId()) - .orElseThrow(() -> new UserNotFoundException("회원 정보를 찾을 수 없습니다.")); - final Long nullableChatRoomId = chatRoomRepository.findByAuctionId(findAuction.getId()) - .map(ChatRoom::getId) - .orElse(DEFAULT_CHAT_ROOM_ID); - - return new ReadAuctionWithChatRoomIdDto( - ReadAuctionDto.from(findAuction), - new ReadChatRoomDto(nullableChatRoomId, isChatParticipant(findAuction, findUser)) + + return ReadAuctionDto.of(findAuction, LocalDateTime.now()); + } + + public ReadAuctionsDto readAllByCondition( + final Pageable pageable, + final ReadAuctionSearchCondition readAuctionSearchCondition) { + final Slice auctions = auctionRepository.findAuctionsAllByCondition( + pageable, + readAuctionSearchCondition ); + + return ReadAuctionsDto.of(auctions, LocalDateTime.now()); } - private boolean isChatParticipant(final Auction findAuction, final User findUser) { - return findAuction.isClosed(LocalDateTime.now()) && findAuction.isSellerOrWinner(findUser, LocalDateTime.now()); + public ReadAuctionsDto readAllByUserId(final Long userId, final Pageable pageable) { + final Slice auctions = auctionRepository.findAuctionsAllByUserId(userId, pageable); + + return ReadAuctionsDto.of(auctions, LocalDateTime.now()); } - public ReadAuctionsDto readAllByLastAuctionId(final Long lastAuctionId, final int size) { - final Slice auctions = auctionRepository.findAuctionsAllByLastAuctionId(lastAuctionId, size); + public ReadAuctionsDto readAllByBidderId(final Long userId, final Pageable pageable) { + final Slice auctions = auctionRepository.findAuctionsAllByBidderId(userId, pageable); - return ReadAuctionsDto.from(auctions); + return ReadAuctionsDto.of(auctions, LocalDateTime.now()); } @Transactional diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/application/dto/ReadAuctionDto.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/application/dto/ReadAuctionDto.java index 67ad1f700..6f9677172 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/application/dto/ReadAuctionDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/application/dto/ReadAuctionDto.java @@ -1,7 +1,9 @@ package com.ddang.ddang.auction.application.dto; import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.AuctionStatus; import com.ddang.ddang.bid.domain.Bid; +import com.ddang.ddang.image.application.util.ImageIdProcessor; import com.ddang.ddang.image.domain.AuctionImage; import java.time.LocalDateTime; @@ -23,12 +25,14 @@ public record ReadAuctionDto( String mainCategory, String subCategory, Long sellerId, - String sellerProfile, + Long sellerProfileId, String sellerName, - double sellerReliability + double sellerReliability, + boolean isSellerDeleted, + AuctionStatus auctionStatus ) { - public static ReadAuctionDto from(final Auction auction) { + public static ReadAuctionDto of(final Auction auction, final LocalDateTime targetTime) { return new ReadAuctionDto( auction.getId(), auction.getTitle(), @@ -40,18 +44,20 @@ public static ReadAuctionDto from(final Auction auction) { auction.getCreatedTime(), auction.getClosingTime(), convertReadRegionsDto(auction), - convertImageUrls(auction), + convertImageIds(auction), auction.getAuctioneerCount(), auction.getSubCategory().getMainCategory().getName(), auction.getSubCategory().getName(), auction.getSeller().getId(), - auction.getSeller().getProfileImage(), + ImageIdProcessor.process(auction.getSeller().getProfileImage()), auction.getSeller().getName(), - auction.getSeller().getReliability() + auction.getSeller().getReliability().getValue(), + auction.getSeller().isDeleted(), + auction.findAuctionStatus(targetTime) ); } - private static List convertImageUrls(final Auction auction) { + private static List convertImageIds(final Auction auction) { return auction.getAuctionImages() .stream() .map(AuctionImage::getId) diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/application/dto/ReadAuctionsDto.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/application/dto/ReadAuctionsDto.java index 4e815f2ca..8635508a6 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/application/dto/ReadAuctionsDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/application/dto/ReadAuctionsDto.java @@ -1,15 +1,16 @@ package com.ddang.ddang.auction.application.dto; import com.ddang.ddang.auction.domain.Auction; +import java.time.LocalDateTime; import java.util.List; import org.springframework.data.domain.Slice; public record ReadAuctionsDto(List readAuctionDtos, boolean isLast) { - public static ReadAuctionsDto from(final Slice auctions) { + public static ReadAuctionsDto of(final Slice auctions, final LocalDateTime targetTime) { final List readAuctionDtos = auctions.getContent() .stream() - .map(ReadAuctionDto::from) + .map(auction -> ReadAuctionDto.of(auction, targetTime)) .toList(); return new ReadAuctionsDto(readAuctionDtos, !auctions.hasNext()); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/AuctionSearchConditionArgumentResolver.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/AuctionSearchConditionArgumentResolver.java new file mode 100644 index 000000000..17203a72b --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/AuctionSearchConditionArgumentResolver.java @@ -0,0 +1,49 @@ +package com.ddang.ddang.auction.configuration; + +import com.ddang.ddang.auction.configuration.exception.InvalidSearchConditionException; +import com.ddang.ddang.auction.presentation.dto.request.ReadAuctionSearchCondition; +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; + +@Component +public class AuctionSearchConditionArgumentResolver implements HandlerMethodArgumentResolver { + + private static final int MINIMUM_TITLE_SEARCH_CONDITION_LENGTH = 2; + private static final int MAXIMUM_TITLE_SEARCH_CONDITION_LENGTH = 30; + + @Override + public boolean supportsParameter(final MethodParameter parameter) { + return parameter.getParameterType().equals(ReadAuctionSearchCondition.class); + } + + @Override + public Object resolveArgument( + final MethodParameter ignoredParameter, + final ModelAndViewContainer ignoredMavContainer, + final NativeWebRequest webRequest, + final WebDataBinderFactory ignoredBinderFactory) + { + final String titleSearchCondition = webRequest.getParameter("title"); + + if (titleSearchCondition == null) { + return new ReadAuctionSearchCondition(null); + } + + validateSearchCondition(titleSearchCondition); + + return new ReadAuctionSearchCondition(titleSearchCondition); + } + + private void validateSearchCondition(final String titleSearchCondition) { + final int length = titleSearchCondition.length(); + + if (length < MINIMUM_TITLE_SEARCH_CONDITION_LENGTH || length > MAXIMUM_TITLE_SEARCH_CONDITION_LENGTH) { + + throw new InvalidSearchConditionException("경매 목록 제목 검색의 길이가 유효하지 않습니다."); + } + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/AuctionWebConfiguration.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/AuctionWebConfiguration.java new file mode 100644 index 000000000..47c712242 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/AuctionWebConfiguration.java @@ -0,0 +1,19 @@ +package com.ddang.ddang.auction.configuration; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@RequiredArgsConstructor +public class AuctionWebConfiguration implements WebMvcConfigurer { + + private final DescendingSortPageableArgumentResolver resolver; + + @Override + public void addArgumentResolvers(final List resolvers) { + resolvers.add(resolver); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/DescendingSort.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/DescendingSort.java new file mode 100644 index 000000000..e4bd80d57 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/DescendingSort.java @@ -0,0 +1,11 @@ +package com.ddang.ddang.auction.configuration; + +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 DescendingSort { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/DescendingSortPageableArgumentResolver.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/DescendingSortPageableArgumentResolver.java new file mode 100644 index 000000000..9fa9c16d5 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/DescendingSortPageableArgumentResolver.java @@ -0,0 +1,65 @@ +package com.ddang.ddang.auction.configuration; + +import com.ddang.ddang.auction.configuration.util.AuctionSortConditionConsts; +import com.ddang.ddang.auction.configuration.util.SortParameter; +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +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; + +@Component +public class DescendingSortPageableArgumentResolver implements HandlerMethodArgumentResolver { + + private static final int DEFAULT_PAGE = 0; + private static final int DEFAULT_SIZE = 10; + + @Override + public boolean supportsParameter(final MethodParameter parameter) { + return parameter.getParameterType().equals(Pageable.class) && + parameter.hasParameterAnnotation(DescendingSort.class); + } + + @Override + public Object resolveArgument( + final MethodParameter ignoredParameter, + final ModelAndViewContainer ignoredMavContainer, + final NativeWebRequest webRequest, + final WebDataBinderFactory ignoredBinderFactory + ) { + final int size = processSizeParameter(webRequest.getParameter("size")); + final int page = processPageParameter(webRequest.getParameter("page")); + final Sort sort = processSortParameter(webRequest.getParameter("sortType")); + + return PageRequest.of(page, size, sort); + } + + private int processSizeParameter(final String sizeParameter) { + if (sizeParameter == null) { + return DEFAULT_SIZE; + } + + return Integer.parseInt(sizeParameter); + } + + private int processPageParameter(final String pageParameter) { + if (pageParameter == null) { + return DEFAULT_PAGE; + } + + return Integer.parseInt(pageParameter) - 1; + } + + private Sort processSortParameter(final String sortParameter) { + if (AuctionSortConditionConsts.CLOSING_TINE.equals(sortParameter)) { + return Sort.by(Direction.ASC, SortParameter.findSortProperty(sortParameter)); + } + + return Sort.by(Direction.DESC, SortParameter.findSortProperty(sortParameter)); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/exception/InvalidSearchConditionException.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/exception/InvalidSearchConditionException.java new file mode 100644 index 000000000..1e061566b --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/exception/InvalidSearchConditionException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.auction.configuration.exception; + +public class InvalidSearchConditionException extends IllegalArgumentException { + + public InvalidSearchConditionException(final String s) { + super(s); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/exception/UnsupportedSortParameterException.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/exception/UnsupportedSortParameterException.java new file mode 100644 index 000000000..caa8e87f9 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/exception/UnsupportedSortParameterException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.auction.configuration.exception; + +public class UnsupportedSortParameterException extends IllegalArgumentException { + + public UnsupportedSortParameterException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/util/AuctionSortConditionConsts.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/util/AuctionSortConditionConsts.java new file mode 100644 index 000000000..a41d9a7c9 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/util/AuctionSortConditionConsts.java @@ -0,0 +1,12 @@ +package com.ddang.ddang.auction.configuration.util; + +public final class AuctionSortConditionConsts { + + public static final String ID = "id"; + public static final String AUCTIONEER_COUNT = "auctioneerCount"; + public static final String CLOSING_TINE = "closingTime"; + public static final String RELIABILITY = "reliability"; + + private AuctionSortConditionConsts() { + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/util/SortParameter.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/util/SortParameter.java new file mode 100644 index 000000000..ff0ba982b --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/configuration/util/SortParameter.java @@ -0,0 +1,43 @@ +package com.ddang.ddang.auction.configuration.util; + +import com.ddang.ddang.auction.configuration.exception.UnsupportedSortParameterException; +import java.util.Arrays; + +public enum SortParameter { + + ID("new", AuctionSortConditionConsts.ID), + AUCTIONEER_COUNT("auctioneer", AuctionSortConditionConsts.AUCTIONEER_COUNT), + CLOSING_TIME("closingTime", AuctionSortConditionConsts.CLOSING_TINE), + RELIABILITY("reliability", AuctionSortConditionConsts.RELIABILITY); + + private final String sortParameter; + private final String sortCondition; + + SortParameter(final String sortParameter, final String sortCondition) { + this.sortParameter = sortParameter; + this.sortCondition = sortCondition; + } + + public static String findSortProperty(final String targetSortParameter) { + if (isDefaultSortCondition(targetSortParameter)) { + return SortParameter.ID.sortCondition; + } + + return Arrays.stream(SortParameter.values()) + .filter(sortParameter -> verifyEquality(targetSortParameter, sortParameter)) + .map(sortParameter -> sortParameter.sortCondition) + .findAny() + .orElseThrow(() -> new UnsupportedSortParameterException("지원하지 않는 정렬 방식입니다.")); + } + + private static boolean isDefaultSortCondition(final String targetSortParameter) { + return targetSortParameter == null || targetSortParameter.isEmpty() || targetSortParameter.isBlank(); + } + + private static boolean verifyEquality( + final String targetSortParameter, + final SortParameter sortParameter + ) { + return sortParameter.sortParameter.equalsIgnoreCase(targetSortParameter); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/domain/Auction.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/domain/Auction.java index 15407d1d9..5d8025753 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/domain/Auction.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/domain/Auction.java @@ -37,7 +37,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @EqualsAndHashCode(callSuper = false, of = {"id"}) -@ToString(of = {"id", "title", "description", "bidUnit", "startPrice", "deleted", "closingTime"}) +@ToString(of = {"id", "title", "description", "bidUnit", "startPrice", "deleted", "closingTime", "auctioneerCount"}) public class Auction extends BaseTimeEntity { private static final boolean DELETED_STATUS = true; @@ -122,6 +122,19 @@ public void addAuctionImages(final List auctionImages) { } } + public AuctionStatus findAuctionStatus(final LocalDateTime targetTime) { + if (targetTime.isBefore(closingTime) && lastBid == null) { + return AuctionStatus.UNBIDDEN; + } + if (targetTime.isBefore(closingTime) && lastBid != null) { + return AuctionStatus.ONGOING; + } + if (targetTime.isAfter(closingTime) && lastBid == null) { + return AuctionStatus.FAILURE ; + } + return AuctionStatus.SUCCESS; + } + public boolean isOwner(final User user) { return this.seller.equals(user); } @@ -132,6 +145,7 @@ public boolean isClosed(final LocalDateTime targetTime) { public boolean isInvalidFirstBidPrice(final BidPrice bidPrice) { final BidPrice startBidPrice = new BidPrice(startPrice.getValue()); + return startBidPrice.isGreaterThan(bidPrice); } @@ -169,4 +183,12 @@ public Optional findWinner(final LocalDateTime targetTime) { private boolean isWinnerExist(final LocalDateTime targetTime) { return auctioneerCount != 0 && isClosed(targetTime); } + + public Optional findLastBidder() { + if (lastBid == null) { + return Optional.empty(); + } + + return Optional.of(lastBid.getBidder()); + } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/domain/AuctionStatus.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/domain/AuctionStatus.java new file mode 100644 index 000000000..e86081ed7 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/domain/AuctionStatus.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.auction.domain; + +public enum AuctionStatus { + UNBIDDEN, + ONGOING, + FAILURE, + SUCCESS +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/JpaAuctionRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/JpaAuctionRepository.java index 6f62a1c72..bc6352f8c 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/JpaAuctionRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/JpaAuctionRepository.java @@ -3,5 +3,9 @@ import com.ddang.ddang.auction.domain.Auction; import org.springframework.data.jpa.repository.JpaRepository; -public interface JpaAuctionRepository extends JpaRepository, QuerydslAuctionRepository { +import java.util.Optional; + +public interface JpaAuctionRepository extends JpaRepository, QuerydslAuctionRepository, QuerydslAuctionAndImageRepository { + + Optional findByIdAndDeletedIsFalse(final Long id); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionAndImageRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionAndImageRepository.java new file mode 100644 index 000000000..8c7c4fe4a --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionAndImageRepository.java @@ -0,0 +1,10 @@ +package com.ddang.ddang.auction.infrastructure.persistence; + +import com.ddang.ddang.auction.infrastructure.persistence.dto.AuctionAndImageDto; + +import java.util.Optional; + +public interface QuerydslAuctionAndImageRepository { + + Optional findDtoByAuctionId(final Long auctionId); +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionAndImageRepositoryImpl.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionAndImageRepositoryImpl.java new file mode 100644 index 000000000..e1a1e79df --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionAndImageRepositoryImpl.java @@ -0,0 +1,46 @@ +package com.ddang.ddang.auction.infrastructure.persistence; + +import com.ddang.ddang.auction.infrastructure.persistence.dto.AuctionAndImageDto; +import com.ddang.ddang.auction.infrastructure.persistence.dto.AuctionAndImageQueryProjectionDto; +import com.ddang.ddang.auction.infrastructure.persistence.dto.QAuctionAndImageQueryProjectionDto; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +import static com.ddang.ddang.auction.domain.QAuction.auction; +import static com.ddang.ddang.bid.domain.QBid.bid; +import static com.ddang.ddang.image.domain.QAuctionImage.auctionImage; + +@Repository +@RequiredArgsConstructor +public class QuerydslAuctionAndImageRepositoryImpl implements QuerydslAuctionAndImageRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public Optional findDtoByAuctionId(final Long auctionId) { + final AuctionAndImageQueryProjectionDto auctionAndImageQueryProjectionDto = + queryFactory.select(new QAuctionAndImageQueryProjectionDto(auction, auctionImage)) + .from(auction) + .leftJoin(auction.lastBid, bid).fetchJoin() + .leftJoin(bid.bidder).fetchJoin() + .leftJoin(auctionImage).on(auctionImage.id.eq( + JPAExpressions + .select(auctionImage.id.min()) + .from(auctionImage) + .where(auctionImage.auction.id.eq(auction.id)) + .groupBy(auctionImage.auction.id) + )).fetchJoin() + .where(auction.id.eq(auctionId)) + .fetchOne(); + + if (auctionAndImageQueryProjectionDto == null) { + return Optional.empty(); + } + + return Optional.of(auctionAndImageQueryProjectionDto.toDto()); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionRepository.java index 7f8cc9f81..593ae4257 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionRepository.java @@ -1,12 +1,21 @@ package com.ddang.ddang.auction.infrastructure.persistence; import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.presentation.dto.request.ReadAuctionSearchCondition; import java.util.Optional; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; public interface QuerydslAuctionRepository { - Slice findAuctionsAllByLastAuctionId(final Long lastAuctionId, final int size); + Slice findAuctionsAllByCondition( + final Pageable pageable, + final ReadAuctionSearchCondition readAuctionSearchCondition + ); Optional findAuctionById(final Long auctionId); + + Slice findAuctionsAllByUserId(final Long userId, final Pageable pageable); + + Slice findAuctionsAllByBidderId(final Long bidderId, final Pageable pageable); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionRepositoryImpl.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionRepositoryImpl.java index 4083caaa4..d6a9aa6e0 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionRepositoryImpl.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionRepositoryImpl.java @@ -1,67 +1,209 @@ package com.ddang.ddang.auction.infrastructure.persistence; import static com.ddang.ddang.auction.domain.QAuction.auction; +import static com.ddang.ddang.bid.domain.QBid.bid; import static com.ddang.ddang.category.domain.QCategory.category; import static com.ddang.ddang.region.domain.QAuctionRegion.auctionRegion; import static com.ddang.ddang.region.domain.QRegion.region; +import com.ddang.ddang.auction.configuration.util.AuctionSortConditionConsts; import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.infrastructure.persistence.exception.UnsupportedSortConditionException; +import com.ddang.ddang.auction.presentation.dto.request.ReadAuctionSearchCondition; import com.ddang.ddang.common.helper.QuerydslSliceHelper; +import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.CaseBuilder; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; import org.springframework.stereotype.Repository; @Repository @RequiredArgsConstructor public class QuerydslAuctionRepositoryImpl implements QuerydslAuctionRepository { + private static final long SLICE_OFFSET = 1L; + private static final int HIGH_PRIORITY = 2; + private static final int LOW_PRIORITY = 1; + private final JPAQueryFactory queryFactory; @Override - public Slice findAuctionsAllByLastAuctionId(final Long lastAuctionId, final int size) { - final List auctions = queryFactory - .selectFrom(auction) - .leftJoin(auction.auctionRegions, auctionRegion).fetchJoin() - .leftJoin(auctionRegion.thirdRegion, region).fetchJoin() - .leftJoin(region.firstRegion).fetchJoin() - .leftJoin(region.secondRegion).fetchJoin() - .leftJoin(auction.subCategory, category).fetchJoin() - .leftJoin(category.mainCategory).fetchJoin() - .leftJoin(auction.seller).fetchJoin() - .where(auction.deleted.isFalse(), lessThanLastAuctionId(lastAuctionId)) - .orderBy(auction.id.desc()) - .limit(size + 1L) - .fetch(); - - return QuerydslSliceHelper.toSlice(auctions, size); + public Slice findAuctionsAllByCondition( + final Pageable pageable, + final ReadAuctionSearchCondition readAuctionSearchCondition + ) { + final List> orderSpecifiers = calculateOrderSpecifiers(pageable); + final List booleanExpressions = calculateBooleanExpressions(readAuctionSearchCondition); + final List findAuctionIds = findAuctionIds(booleanExpressions, orderSpecifiers, pageable); + final List findAuctions = findAuctionsByIdsAndOrderSpecifiers(findAuctionIds, orderSpecifiers); + + return QuerydslSliceHelper.toSlice(findAuctions, pageable); + } + + private List> calculateOrderSpecifiers(final Pageable pageable) { + final List> orderSpecifiers = new ArrayList<>(processOrderSpecifiers(pageable)); + + orderSpecifiers.add(auction.id.desc()); + + return orderSpecifiers; + } + + private List> processOrderSpecifiers(final Pageable pageable) { + final List> orderSpecifiers = new ArrayList<>(); + final Sort sort = pageable.getSort(); + + for (final Order order : sort) { + if (AuctionSortConditionConsts.ID.equals(order.getProperty())) { + return Collections.emptyList(); + } + + orderSpecifiers.addAll(processOrderSpecifierByCondition(order)); + } + + return orderSpecifiers; + } + + private List> processOrderSpecifierByCondition(final Order order) { + if (AuctionSortConditionConsts.RELIABILITY.equals(order.getProperty())) { + return List.of(closingTimeOrderSpecifier(), auction.seller.reliability.value.desc()); + } + if (AuctionSortConditionConsts.AUCTIONEER_COUNT.equals(order.getProperty())) { + return List.of(closingTimeOrderSpecifier(), auction.auctioneerCount.desc()); + } + if (AuctionSortConditionConsts.CLOSING_TINE.equals(order.getProperty())) { + return List.of(closingTimeOrderSpecifier(), auction.closingTime.asc()); + } + + throw new UnsupportedSortConditionException("지원하지 않는 정렬 방식입니다."); + } + + private OrderSpecifier closingTimeOrderSpecifier() { + final LocalDateTime now = LocalDateTime.now(); + + return new CaseBuilder() + .when(auction.closingTime.after(now)).then(LOW_PRIORITY) + .otherwise(HIGH_PRIORITY) + .asc(); + } + + private List calculateBooleanExpressions(final ReadAuctionSearchCondition searchCondition) { + final List booleanExpressions = new ArrayList<>(); + + booleanExpressions.add(auction.deleted.isFalse()); + + final BooleanExpression titleBooleanExpression = convertTitleSearchCondition(searchCondition); + + if (titleBooleanExpression != null) { + booleanExpressions.add(titleBooleanExpression); + } + + return booleanExpressions; } - private BooleanExpression lessThanLastAuctionId(final Long lastAuctionId) { - if (lastAuctionId == null) { + private List findAuctionIds( + final List booleanExpressions, + final List> orderSpecifiers, + final Pageable pageable + ) { + return queryFactory.select(auction.id) + .from(auction) + .where(booleanExpressions.toArray(BooleanExpression[]::new)) + .orderBy(orderSpecifiers.toArray(OrderSpecifier[]::new)) + .limit(pageable.getPageSize() + SLICE_OFFSET) + .offset(pageable.getOffset()) + .fetch(); + } + + private BooleanExpression convertTitleSearchCondition(final ReadAuctionSearchCondition readAuctionSearchCondition) { + final String titleSearchCondition = readAuctionSearchCondition.title(); + + if (titleSearchCondition == null) { return null; } - return auction.id.lt(lastAuctionId); + return auction.title.like("%" + titleSearchCondition + "%"); + } + + private List findAuctionsByIdsAndOrderSpecifiers( + final List targetIds, + final List> orderSpecifiers + ) { + return queryFactory.selectFrom(auction) + .leftJoin(auction.auctionRegions, auctionRegion).fetchJoin() + .leftJoin(auctionRegion.thirdRegion, region).fetchJoin() + .leftJoin(region.firstRegion).fetchJoin() + .leftJoin(region.secondRegion).fetchJoin() + .leftJoin(auction.subCategory, category).fetchJoin() + .leftJoin(category.mainCategory).fetchJoin() + .leftJoin(auction.seller).fetchJoin() + .leftJoin(auction.lastBid).fetchJoin() + .where(auction.id.in(targetIds.toArray(Long[]::new))) + .orderBy(orderSpecifiers.toArray(OrderSpecifier[]::new)) + .fetch(); } @Override public Optional findAuctionById(final Long auctionId) { - final Auction findAuction = queryFactory - .selectFrom(auction) - .leftJoin(auction.auctionRegions, auctionRegion).fetchJoin() - .leftJoin(auctionRegion.thirdRegion, region).fetchJoin() - .leftJoin(region.firstRegion).fetchJoin() - .leftJoin(region.secondRegion).fetchJoin() - .leftJoin(auction.subCategory, category).fetchJoin() - .leftJoin(category.mainCategory).fetchJoin() - .leftJoin(auction.seller).fetchJoin() - .where(auction.deleted.isFalse(), auction.id.eq(auctionId)) - .fetchOne(); + final Auction findAuction = queryFactory.selectFrom(auction) + .leftJoin(auction.auctionRegions, auctionRegion).fetchJoin() + .leftJoin(auctionRegion.thirdRegion, region).fetchJoin() + .leftJoin(region.firstRegion).fetchJoin() + .leftJoin(region.secondRegion).fetchJoin() + .leftJoin(auction.subCategory, category).fetchJoin() + .leftJoin(category.mainCategory).fetchJoin() + .leftJoin(auction.seller).fetchJoin() + .leftJoin(auction.lastBid).fetchJoin() + .where(auction.deleted.isFalse(), auction.id.eq(auctionId)) + .fetchOne(); return Optional.ofNullable(findAuction); } + + @Override + public Slice findAuctionsAllByUserId(final Long userId, final Pageable pageable) { + final List booleanExpressions = List.of( + auction.seller.id.eq(userId), + auction.deleted.isFalse() + ); + final List> orderSpecifiers = List.of(auction.id.desc()); + final List findAuctionIds = findAuctionIds(booleanExpressions, orderSpecifiers, pageable); + final List findAuctions = findAuctionsByIdsAndOrderSpecifiers( + findAuctionIds, + List.of(auction.id.desc()) + ); + + return QuerydslSliceHelper.toSlice(findAuctions, pageable); + } + + @Override + public Slice findAuctionsAllByBidderId(final Long bidderId, final Pageable pageable) { + final List findAuctionIds = queryFactory.select(bid.auction.id) + .from(bid) + .where(bid.bidder.id.eq(bidderId)) + .groupBy(bid.auction.id) + .orderBy(bid.id.max().desc()) + .limit(pageable.getPageSize() + SLICE_OFFSET) + .offset(pageable.getOffset()) + .fetch(); + final List findAuctions = findAuctionsByIdsAndOrderSpecifiers(findAuctionIds, Collections.emptyList()); + + findAuctions.sort((firstAuction, secondAuction) -> { + int firstAuctionIndex = findAuctionIds.indexOf(firstAuction.getId()); + int secondAuctionIndex = findAuctionIds.indexOf(secondAuction.getId()); + + return Integer.compare(firstAuctionIndex, secondAuctionIndex); + }); + + return QuerydslSliceHelper.toSlice(findAuctions, pageable); + } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/dto/AuctionAndImageDto.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/dto/AuctionAndImageDto.java new file mode 100644 index 000000000..2615a219e --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/dto/AuctionAndImageDto.java @@ -0,0 +1,7 @@ +package com.ddang.ddang.auction.infrastructure.persistence.dto; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.image.domain.AuctionImage; + +public record AuctionAndImageDto(Auction auction, AuctionImage auctionImage) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/dto/AuctionAndImageQueryProjectionDto.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/dto/AuctionAndImageQueryProjectionDto.java new file mode 100644 index 000000000..85300e897 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/dto/AuctionAndImageQueryProjectionDto.java @@ -0,0 +1,17 @@ +package com.ddang.ddang.auction.infrastructure.persistence.dto; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.image.domain.AuctionImage; +import com.querydsl.core.annotations.QueryProjection; + +public record AuctionAndImageQueryProjectionDto(Auction auction, AuctionImage auctionImage) { + + @QueryProjection + public AuctionAndImageQueryProjectionDto { + } + + // TODO: 2023/09/22 dto이름 정해지면 명확한 dto이름으로 바꾸기 + public AuctionAndImageDto toDto() { + return new AuctionAndImageDto(this.auction, this.auctionImage); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/exception/UnsupportedSortConditionException.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/exception/UnsupportedSortConditionException.java new file mode 100644 index 000000000..336072ceb --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/exception/UnsupportedSortConditionException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.auction.infrastructure.persistence.exception; + +public class UnsupportedSortConditionException extends IllegalArgumentException { + + public UnsupportedSortConditionException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/AuctionController.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/AuctionController.java index d7e869bc0..fa3dad2fd 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/AuctionController.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/AuctionController.java @@ -3,43 +3,45 @@ import com.ddang.ddang.auction.application.AuctionService; import com.ddang.ddang.auction.application.dto.CreateAuctionDto; import com.ddang.ddang.auction.application.dto.CreateInfoAuctionDto; -import com.ddang.ddang.auction.application.dto.ReadAuctionWithChatRoomIdDto; +import com.ddang.ddang.auction.application.dto.ReadAuctionDto; import com.ddang.ddang.auction.application.dto.ReadAuctionsDto; +import com.ddang.ddang.auction.application.dto.ReadChatRoomDto; +import com.ddang.ddang.auction.configuration.DescendingSort; import com.ddang.ddang.auction.presentation.dto.request.CreateAuctionRequest; +import com.ddang.ddang.auction.presentation.dto.request.ReadAuctionSearchCondition; import com.ddang.ddang.auction.presentation.dto.response.CreateAuctionResponse; import com.ddang.ddang.auction.presentation.dto.response.ReadAuctionDetailResponse; import com.ddang.ddang.auction.presentation.dto.response.ReadAuctionsResponse; import com.ddang.ddang.authentication.configuration.AuthenticateUser; import com.ddang.ddang.authentication.domain.dto.AuthenticationUserInfo; +import com.ddang.ddang.chat.application.ChatRoomService; import jakarta.validation.Valid; +import java.net.URI; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import java.net.URI; -import java.util.List; - @RestController @RequestMapping("/auctions") @RequiredArgsConstructor public class AuctionController { - private static final String AUCTIONS_IMAGE_BASE_URL = "/auctions/images/"; - private final AuctionService auctionService; + private final ChatRoomService chatRoomService; @PostMapping public ResponseEntity create( - @AuthenticateUser AuthenticationUserInfo userInfo, + @AuthenticateUser final AuthenticationUserInfo userInfo, @RequestPart final List images, @RequestPart @Valid final CreateAuctionRequest request ) { @@ -48,7 +50,7 @@ public ResponseEntity create( images, userInfo.userId() )); - final CreateAuctionResponse response = CreateAuctionResponse.of(createInfoAuctionDto, calculateBaseImageUrl()); + final CreateAuctionResponse response = CreateAuctionResponse.from(createInfoAuctionDto); return ResponseEntity.created(URI.create("/auctions/" + createInfoAuctionDto.id())) .body(response); @@ -59,39 +61,39 @@ public ResponseEntity read( @AuthenticateUser final AuthenticationUserInfo userInfo, @PathVariable final Long auctionId ) { - final ReadAuctionWithChatRoomIdDto readAuctionDto = auctionService.readByAuctionId(auctionId, userInfo); + final ReadAuctionDto readAuctionDto = auctionService.readByAuctionId(auctionId); + final ReadChatRoomDto readChatRoomDto = chatRoomService.readChatInfoByAuctionId(auctionId, userInfo); final ReadAuctionDetailResponse response = ReadAuctionDetailResponse.of( readAuctionDto, - calculateBaseImageUrl(), - userInfo + userInfo, + readChatRoomDto ); return ResponseEntity.ok(response); } @GetMapping - public ResponseEntity readAllByLastAuctionId( - @RequestParam(required = false) final Long lastAuctionId, - @RequestParam(required = false, defaultValue = "10") final int size + public ResponseEntity readAllByCondition( + @AuthenticateUser final AuthenticationUserInfo ignored, + @DescendingSort final Pageable pageable, + final ReadAuctionSearchCondition readAuctionSearchCondition ) { - final ReadAuctionsDto readAuctionsDto = auctionService.readAllByLastAuctionId(lastAuctionId, size); - final ReadAuctionsResponse response = ReadAuctionsResponse.of(readAuctionsDto, calculateBaseImageUrl()); + final ReadAuctionsDto readAuctionsDto = auctionService.readAllByCondition( + pageable, + readAuctionSearchCondition + ); + final ReadAuctionsResponse response = ReadAuctionsResponse.from(readAuctionsDto); return ResponseEntity.ok(response); } @DeleteMapping("/{auctionId}") public ResponseEntity delete( - @AuthenticateUser AuthenticationUserInfo userInfo, + @AuthenticateUser final AuthenticationUserInfo userInfo, @PathVariable final Long auctionId ) { auctionService.deleteByAuctionId(auctionId, userInfo.userId()); return ResponseEntity.noContent().build(); } - - private String calculateBaseImageUrl() { - return ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString() - .concat(AUCTIONS_IMAGE_BASE_URL); - } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/AuctionQnaController.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/AuctionQnaController.java new file mode 100644 index 000000000..42f9da78f --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/AuctionQnaController.java @@ -0,0 +1,27 @@ +package com.ddang.ddang.auction.presentation; + +import com.ddang.ddang.auction.presentation.dto.response.ReadQnasResponse; +import com.ddang.ddang.qna.application.QuestionService; +import com.ddang.ddang.qna.application.dto.ReadQnasDto; +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; + +@RestController +@RequestMapping("/auctions") +@RequiredArgsConstructor +public class AuctionQnaController { + + private final QuestionService questionService; + + @GetMapping("/{auctionId}/questions") + public ResponseEntity readAllByAuctionId(@PathVariable final Long auctionId) { + final ReadQnasDto readQnasDto = questionService.readAllByAuctionId(auctionId); + final ReadQnasResponse response = ReadQnasResponse.from(readQnasDto); + + return ResponseEntity.ok(response); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/AuctionReviewController.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/AuctionReviewController.java new file mode 100644 index 000000000..4486c41a8 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/AuctionReviewController.java @@ -0,0 +1,32 @@ +package com.ddang.ddang.auction.presentation; + +import com.ddang.ddang.auction.presentation.dto.response.ReadReviewDetailResponse; +import com.ddang.ddang.authentication.configuration.AuthenticateUser; +import com.ddang.ddang.authentication.domain.dto.AuthenticationUserInfo; +import com.ddang.ddang.review.application.ReviewService; +import com.ddang.ddang.review.application.dto.ReadReviewDetailDto; +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; + +@RestController +@RequestMapping("/auctions") +@RequiredArgsConstructor +public class AuctionReviewController { + + private final ReviewService reviewService; + + @GetMapping("/{auctionId}/reviews") + public ResponseEntity readByAuctionId( + @AuthenticateUser final AuthenticationUserInfo userInfo, + @PathVariable final Long auctionId + ) { + final ReadReviewDetailDto readReviewDetailDto = reviewService.readByAuctionIdAndWriterId(userInfo.userId(), auctionId); + ReadReviewDetailResponse response = ReadReviewDetailResponse.from(readReviewDetailDto); + + return ResponseEntity.ok(response); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/request/CreateAuctionRequest.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/request/CreateAuctionRequest.java index 7a0d51ddd..753fff9b5 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/request/CreateAuctionRequest.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/request/CreateAuctionRequest.java @@ -4,6 +4,7 @@ import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; + import java.time.LocalDateTime; import java.util.List; @@ -15,11 +16,11 @@ public record CreateAuctionRequest( String description, @NotNull(message = "입찰 단위가 입력되지 않았습니다.") - @Positive(message = "금액은 음수를 입력할 수 없습니다.") + @Positive(message = "금액은 양수로 입력해주세요.") Integer bidUnit, @NotNull(message = "시작가가 입력되지 않았습니다.") - @Positive(message = "금액은 음수를 입력할 수 없습니다.") + @Positive(message = "금액은 양수로 입력해주세요.") Integer startPrice, @NotNull(message = "마감 시간이 입력되지 않았습니다.") @@ -27,7 +28,7 @@ public record CreateAuctionRequest( LocalDateTime closingTime, @NotNull(message = "하위 카테고리가 입력되지 않았습니다.") - @Positive(message = "카테고리 아이디는 음수 또는 0을 입력할 수 없습니다.") + @Positive(message = "잘못된 카테고리 입니다.") Long subCategoryId, List thirdRegionIds diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/request/ReadAuctionSearchCondition.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/request/ReadAuctionSearchCondition.java new file mode 100644 index 000000000..77dcee403 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/request/ReadAuctionSearchCondition.java @@ -0,0 +1,4 @@ +package com.ddang.ddang.auction.presentation.dto.request; + +public record ReadAuctionSearchCondition(String title) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/AuctionDetailResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/AuctionDetailResponse.java index a6db32fd3..b4092b2e5 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/AuctionDetailResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/AuctionDetailResponse.java @@ -1,6 +1,8 @@ package com.ddang.ddang.auction.presentation.dto.response; import com.ddang.ddang.auction.application.dto.ReadAuctionDto; +import com.ddang.ddang.image.presentation.util.ImageRelativeUrl; +import com.ddang.ddang.image.presentation.util.ImageUrlCalculator; import com.fasterxml.jackson.annotation.JsonFormat; import java.time.LocalDateTime; @@ -36,16 +38,16 @@ public record AuctionDetailResponse( int auctioneerCount ) { - public static AuctionDetailResponse of(final ReadAuctionDto dto, final String baseUrl) { + public static AuctionDetailResponse from(final ReadAuctionDto dto) { return new AuctionDetailResponse( dto.id(), - convertImageUrls(dto, baseUrl), + convertImageFullUrls(dto), dto.title(), new CategoryResponse(dto.mainCategory(), dto.subCategory()), dto.description(), dto.startPrice(), dto.lastBidPrice(), - processAuctionStatus(dto.closingTime(), dto.lastBidPrice()), + dto.auctionStatus().name(), dto.bidUnit(), dto.registerTime(), dto.closingTime(), @@ -54,10 +56,10 @@ public static AuctionDetailResponse of(final ReadAuctionDto dto, final String ba ); } - private static List convertImageUrls(final ReadAuctionDto dto, final String baseUrl) { + private static List convertImageFullUrls(final ReadAuctionDto dto) { return dto.auctionImageIds() .stream() - .map(id -> baseUrl.concat(String.valueOf(id))) + .map(id -> ImageUrlCalculator.calculateBy(ImageRelativeUrl.AUCTION, id)) .toList(); } @@ -67,18 +69,4 @@ private static List convertDirectRegionsResponse(final Rea .map(DirectRegionResponse::from) .toList(); } - - // TODO 2차 데모데이 이후 enum으로 처리 - private static String processAuctionStatus(final LocalDateTime closingTime, final Integer lastBidPrice) { - if (LocalDateTime.now().isBefore(closingTime) && lastBidPrice == null) { - return "UNBIDDEN"; - } - if (LocalDateTime.now().isBefore(closingTime) && lastBidPrice != null) { - return "ONGOING"; - } - if (LocalDateTime.now().isAfter(closingTime) && lastBidPrice == null) { - return "FAILURE"; - } - return "SUCCESS"; - } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/CategoryResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/CategoryResponse.java index 2d76554fd..ec5de6718 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/CategoryResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/CategoryResponse.java @@ -1,5 +1,4 @@ package com.ddang.ddang.auction.presentation.dto.response; -// TODO 2차 데모데이 이후 리펙터링 예정 public record CategoryResponse(String main, String sub) { } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ChatRoomInAuctionResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ChatRoomInAuctionResponse.java index 6f8b99b13..952f94a52 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ChatRoomInAuctionResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ChatRoomInAuctionResponse.java @@ -5,7 +5,6 @@ public record ChatRoomInAuctionResponse(Long id, boolean isChatParticipant) { public static ChatRoomInAuctionResponse from(final ReadChatRoomDto readChatRoomDto) { - return new ChatRoomInAuctionResponse(readChatRoomDto.id(), readChatRoomDto.isChatParticipant()); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/CreateAuctionResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/CreateAuctionResponse.java index a6eb9e8b2..2f3c0c9d2 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/CreateAuctionResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/CreateAuctionResponse.java @@ -1,6 +1,9 @@ package com.ddang.ddang.auction.presentation.dto.response; import com.ddang.ddang.auction.application.dto.CreateInfoAuctionDto; +import com.ddang.ddang.auction.domain.AuctionStatus; +import com.ddang.ddang.image.presentation.util.ImageRelativeUrl; +import com.ddang.ddang.image.presentation.util.ImageUrlCalculator; public record CreateAuctionResponse( Long id, @@ -11,15 +14,18 @@ public record CreateAuctionResponse( int auctioneerCount ) { - public static CreateAuctionResponse of(final CreateInfoAuctionDto dto, final String baseUrl) { + public static CreateAuctionResponse from(final CreateInfoAuctionDto dto) { return new CreateAuctionResponse( dto.id(), dto.title(), - baseUrl.concat(String.valueOf(dto.auctionImageId())), + convertAuctionImageUrl(dto.auctionImageId()), dto.startPrice(), - // TODO 2차 데모데이 이후 enum으로 처리 - "UNBIDDEN", + AuctionStatus.UNBIDDEN.name(), 0 ); } + + private static String convertAuctionImageUrl(final Long id) { + return ImageUrlCalculator.calculateBy(ImageRelativeUrl.AUCTION, id); + } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadAnswerResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadAnswerResponse.java new file mode 100644 index 000000000..34ebc24a1 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadAnswerResponse.java @@ -0,0 +1,27 @@ +package com.ddang.ddang.auction.presentation.dto.response; + +import com.ddang.ddang.qna.application.dto.ReadAnswerDto; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDateTime; + +public record ReadAnswerResponse( + Long id, + + ReadUserInAuctionQuestionResponse writer, + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul") + LocalDateTime createdTime, + + String content +) { + + public static ReadAnswerResponse from(final ReadAnswerDto readAnswerDto) { + return new ReadAnswerResponse( + readAnswerDto.id(), + ReadUserInAuctionQuestionResponse.from(readAnswerDto.writerDto()), + readAnswerDto.createdTime(), + readAnswerDto.content() + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadAuctionDetailResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadAuctionDetailResponse.java index 4ab1bb635..318daccce 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadAuctionDetailResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadAuctionDetailResponse.java @@ -1,6 +1,7 @@ package com.ddang.ddang.auction.presentation.dto.response; -import com.ddang.ddang.auction.application.dto.ReadAuctionWithChatRoomIdDto; +import com.ddang.ddang.auction.application.dto.ReadAuctionDto; +import com.ddang.ddang.auction.application.dto.ReadChatRoomDto; import com.ddang.ddang.authentication.domain.dto.AuthenticationUserInfo; public record ReadAuctionDetailResponse( @@ -11,28 +12,23 @@ public record ReadAuctionDetailResponse( ) { public static ReadAuctionDetailResponse of( - final ReadAuctionWithChatRoomIdDto dto, - final String baseUrl, - final AuthenticationUserInfo userInfo + final ReadAuctionDto auctionDto, + final AuthenticationUserInfo userInfo, + final ReadChatRoomDto chatRoomDto ) { - final AuctionDetailResponse auctionDetailResponse = AuctionDetailResponse.of(dto.auctionDto(), baseUrl); - final SellerResponse sellerResponse = new SellerResponse( - dto.auctionDto().sellerId(), - dto.auctionDto().sellerProfile(), - dto.auctionDto().sellerName(), - dto.auctionDto().sellerReliability() - ); - final ChatRoomInAuctionResponse chatRoomResponse = ChatRoomInAuctionResponse.from(dto.chatRoomDto()); + final AuctionDetailResponse auctionDetailResponse = AuctionDetailResponse.from(auctionDto); + final SellerResponse sellerResponse = SellerResponse.from(auctionDto); + final ChatRoomInAuctionResponse chatRoomResponse = ChatRoomInAuctionResponse.from(chatRoomDto); return new ReadAuctionDetailResponse( auctionDetailResponse, sellerResponse, chatRoomResponse, - isOwner(dto, userInfo) + isOwner(auctionDto, userInfo) ); } - private static boolean isOwner(final ReadAuctionWithChatRoomIdDto dto, final AuthenticationUserInfo userInfo) { - return dto.auctionDto().sellerId().equals(userInfo.userId()); + private static boolean isOwner(final ReadAuctionDto auctionDto, final AuthenticationUserInfo userInfo) { + return auctionDto.sellerId().equals(userInfo.userId()); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadAuctionResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadAuctionResponse.java index d3e84a3a6..a11aaaf06 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadAuctionResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadAuctionResponse.java @@ -1,9 +1,10 @@ package com.ddang.ddang.auction.presentation.dto.response; import com.ddang.ddang.auction.application.dto.ReadAuctionDto; +import com.ddang.ddang.image.presentation.util.ImageRelativeUrl; +import com.ddang.ddang.image.presentation.util.ImageUrlCalculator; -import java.time.LocalDateTime; - +// TODO: 9/29/23 추후 대표 이미지 관련 필드 추가 public record ReadAuctionResponse( Long id, String title, @@ -13,21 +14,17 @@ public record ReadAuctionResponse( int auctioneerCount ) { - public static ReadAuctionResponse of(final ReadAuctionDto dto, final String baseUrl) { + public static ReadAuctionResponse from(final ReadAuctionDto dto) { return new ReadAuctionResponse( dto.id(), dto.title(), - convertImageUrl(dto, baseUrl), + ImageUrlCalculator.calculateBy(ImageRelativeUrl.AUCTION, dto.auctionImageIds().get(0)), processAuctionPrice(dto.startPrice(), dto.lastBidPrice()), - processAuctionStatus(dto.closingTime(), dto.lastBidPrice()), + dto.auctionStatus().name(), dto.auctioneerCount() ); } - private static String convertImageUrl(final ReadAuctionDto dto, final String baseUrl) { - return baseUrl.concat(String.valueOf(dto.auctionImageIds().get(0))); - } - private static int processAuctionPrice(final Integer startPrice, final Integer lastBidPrice) { if (lastBidPrice == null) { return startPrice; @@ -35,18 +32,4 @@ private static int processAuctionPrice(final Integer startPrice, final Integer l return lastBidPrice; } - - // TODO 2차 데모데이 이후 enum으로 처리 - private static String processAuctionStatus(final LocalDateTime closingTime, final Integer lastBidPrice) { - if (LocalDateTime.now().isBefore(closingTime) && lastBidPrice == null) { - return "UNBIDDEN"; - } - if (LocalDateTime.now().isBefore(closingTime) && lastBidPrice != null) { - return "ONGOING"; - } - if (LocalDateTime.now().isAfter(closingTime) && lastBidPrice == null) { - return "FAILURE"; - } - return "SUCCESS"; - } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadAuctionsResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadAuctionsResponse.java index 1f1f6d401..ae62f86b9 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadAuctionsResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadAuctionsResponse.java @@ -1,16 +1,15 @@ package com.ddang.ddang.auction.presentation.dto.response; import com.ddang.ddang.auction.application.dto.ReadAuctionsDto; + import java.util.List; public record ReadAuctionsResponse(List auctions, boolean isLast) { - public static ReadAuctionsResponse of(final ReadAuctionsDto readAuctionsDto, final String baseUrl) { + public static ReadAuctionsResponse from(final ReadAuctionsDto readAuctionsDto) { final List readAuctionResponses = readAuctionsDto.readAuctionDtos() .stream() - .map(dto -> ReadAuctionResponse.of( - dto, baseUrl - )) + .map(ReadAuctionResponse::from) .toList(); return new ReadAuctionsResponse(readAuctionResponses, readAuctionsDto.isLast()); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadQnaResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadQnaResponse.java new file mode 100644 index 000000000..dd71a5eca --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadQnaResponse.java @@ -0,0 +1,25 @@ +package com.ddang.ddang.auction.presentation.dto.response; + +import com.ddang.ddang.qna.application.dto.ReadAnswerDto; +import com.ddang.ddang.qna.application.dto.ReadQnaDto; + +public record ReadQnaResponse( + ReadQuestionResponse question, + ReadAnswerResponse answer +) { + + public static ReadQnaResponse from(final ReadQnaDto readQnaDto) { + final ReadQuestionResponse question = ReadQuestionResponse.from(readQnaDto.readQuestionDto()); + final ReadAnswerResponse answer = processReadAnswerResponse(readQnaDto.readAnswerDto()); + + return new ReadQnaResponse(question, answer); + } + + private static ReadAnswerResponse processReadAnswerResponse(final ReadAnswerDto readAnswerDto) { + if (readAnswerDto == null) { + return null; + } + + return ReadAnswerResponse.from(readAnswerDto); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadQnasResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadQnasResponse.java new file mode 100644 index 000000000..d390d0fb4 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadQnasResponse.java @@ -0,0 +1,18 @@ +package com.ddang.ddang.auction.presentation.dto.response; + +import com.ddang.ddang.qna.application.dto.ReadQnaDto; +import com.ddang.ddang.qna.application.dto.ReadQnasDto; + +import java.util.List; + +public record ReadQnasResponse(List qnas) { + + public static ReadQnasResponse from(final ReadQnasDto readQnasDto) { + final List dtos = readQnasDto.readQnaDtos(); + final List readQnaResponses = dtos.stream() + .map(ReadQnaResponse::from) + .toList(); + + return new ReadQnasResponse(readQnaResponses); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadQuestionResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadQuestionResponse.java new file mode 100644 index 000000000..9a3305a8a --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadQuestionResponse.java @@ -0,0 +1,27 @@ +package com.ddang.ddang.auction.presentation.dto.response; + +import com.ddang.ddang.qna.application.dto.ReadQuestionDto; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDateTime; + +public record ReadQuestionResponse( + Long id, + + ReadUserInAuctionQuestionResponse writer, + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul") + LocalDateTime createdTime, + + String content +) { + + public static ReadQuestionResponse from(final ReadQuestionDto readQuestionDto) { + return new ReadQuestionResponse( + readQuestionDto.id(), + ReadUserInAuctionQuestionResponse.from(readQuestionDto.readUserInQnaDto()), + readQuestionDto.createdTime(), + readQuestionDto.content() + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadReviewDetailResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadReviewDetailResponse.java new file mode 100644 index 000000000..b50f0e3f1 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadReviewDetailResponse.java @@ -0,0 +1,16 @@ +package com.ddang.ddang.auction.presentation.dto.response; + +import com.ddang.ddang.review.application.dto.ReadReviewDetailDto; +import jakarta.annotation.Nullable; + +public record ReadReviewDetailResponse(@Nullable Float score, @Nullable String content) { + + public static ReadReviewDetailResponse from(final ReadReviewDetailDto readReviewDetailDto) { + final Double nullableScore = readReviewDetailDto.score(); + if(nullableScore == null) { + return new ReadReviewDetailResponse(null, readReviewDetailDto.content()); + } + + return new ReadReviewDetailResponse(nullableScore.floatValue(), readReviewDetailDto.content()); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadUserInAuctionQuestionResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadUserInAuctionQuestionResponse.java new file mode 100644 index 000000000..b9f3d7051 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadUserInAuctionQuestionResponse.java @@ -0,0 +1,18 @@ +package com.ddang.ddang.auction.presentation.dto.response; + +import com.ddang.ddang.image.presentation.util.ImageRelativeUrl; +import com.ddang.ddang.image.presentation.util.ImageUrlCalculator; +import com.ddang.ddang.qna.application.dto.ReadUserInQnaDto; + +public record ReadUserInAuctionQuestionResponse(Long id, String name, String image) { + + public static ReadUserInAuctionQuestionResponse from( + final ReadUserInQnaDto writerDto + ) { + return new ReadUserInAuctionQuestionResponse( + writerDto.id(), + writerDto.name(), + ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, writerDto.profileImageId()) + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/SellerResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/SellerResponse.java index 5d99cb026..37ebc7804 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/SellerResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/SellerResponse.java @@ -1,9 +1,24 @@ package com.ddang.ddang.auction.presentation.dto.response; +import com.ddang.ddang.auction.application.dto.ReadAuctionDto; +import com.ddang.ddang.image.presentation.util.ImageRelativeUrl; +import com.ddang.ddang.image.presentation.util.ImageUrlCalculator; + public record SellerResponse( Long id, String image, String nickname, - double reliability + Float reliability ) { + + public static SellerResponse from(final ReadAuctionDto auctionDto) { + final Float floatReliability = Float.valueOf(String.valueOf(auctionDto.sellerReliability())); + + return new SellerResponse( + auctionDto.sellerId(), + ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, auctionDto.sellerProfileId()), + auctionDto.sellerName(), + floatReliability + ); + } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/authentication/application/AuthenticationService.java b/backend/ddang/src/main/java/com/ddang/ddang/authentication/application/AuthenticationService.java index 77d473377..58add0567 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/authentication/application/AuthenticationService.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/authentication/application/AuthenticationService.java @@ -1,23 +1,35 @@ package com.ddang.ddang.authentication.application; import com.ddang.ddang.authentication.application.dto.TokenDto; -import com.ddang.ddang.authentication.domain.exception.InvalidTokenException; +import com.ddang.ddang.authentication.application.exception.InvalidWithdrawalException; +import com.ddang.ddang.authentication.application.util.RandomNameGenerator; import com.ddang.ddang.authentication.domain.Oauth2UserInformationProviderComposite; -import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims; import com.ddang.ddang.authentication.domain.TokenDecoder; import com.ddang.ddang.authentication.domain.TokenEncoder; import com.ddang.ddang.authentication.domain.TokenType; import com.ddang.ddang.authentication.domain.dto.UserInformationDto; +import com.ddang.ddang.authentication.domain.exception.InvalidTokenException; +import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims; import com.ddang.ddang.authentication.infrastructure.oauth2.OAuth2UserInformationProvider; import com.ddang.ddang.authentication.infrastructure.oauth2.Oauth2Type; +import com.ddang.ddang.device.application.DeviceTokenService; +import com.ddang.ddang.device.application.dto.PersistDeviceTokenDto; +import com.ddang.ddang.device.infrastructure.persistence.JpaDeviceTokenRepository; +import com.ddang.ddang.image.application.exception.ImageNotFoundException; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.image.infrastructure.persistence.JpaProfileImageRepository; +import com.ddang.ddang.user.domain.Reliability; import com.ddang.ddang.user.domain.User; import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; -import java.time.LocalDateTime; -import java.util.Map; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; +import java.util.Map; + +import static com.ddang.ddang.image.domain.ProfileImage.DEFAULT_PROFILE_IMAGE_STORE_NAME; + @Service @Transactional(readOnly = true) @RequiredArgsConstructor @@ -25,27 +37,38 @@ public class AuthenticationService { private static final String PRIVATE_CLAIMS_KEY = "userId"; + private final DeviceTokenService deviceTokenService; private final Oauth2UserInformationProviderComposite providerComposite; private final JpaUserRepository userRepository; + private final JpaProfileImageRepository profileImageRepository; private final TokenEncoder tokenEncoder; private final TokenDecoder tokenDecoder; + private final BlackListTokenService blackListTokenService; + private final JpaDeviceTokenRepository deviceTokenRepository; @Transactional - public TokenDto login(final Oauth2Type oauth2Type, final String oauth2AccessToken) { + public TokenDto login(final Oauth2Type oauth2Type, final String oauth2AccessToken, final String deviceToken) { final OAuth2UserInformationProvider provider = providerComposite.findProvider(oauth2Type); final UserInformationDto userInformationDto = provider.findUserInformation(oauth2AccessToken); final User persistUser = findOrPersistUser(oauth2Type, userInformationDto); + updateOrPersistDeviceToken(deviceToken, persistUser); + return convertTokenDto(persistUser); } + private void updateOrPersistDeviceToken(final String deviceToken, final User persistUser) { + final PersistDeviceTokenDto persistDeviceTokenDto = new PersistDeviceTokenDto(deviceToken); + deviceTokenService.persist(persistUser.getId(), persistDeviceTokenDto); + } + private User findOrPersistUser(final Oauth2Type oauth2Type, final UserInformationDto userInformationDto) { - return userRepository.findByOauthId(userInformationDto.findUserId()) + return userRepository.findByOauthIdAndDeletedIsFalse(userInformationDto.findUserId()) .orElseGet(() -> { final User user = User.builder() - .name(oauth2Type.calculateNickname(userInformationDto)) - .profileImage(null) - .reliability(0.0d) + .name(oauth2Type.calculateNickname(calculateRandomNumber())) + .profileImage(findDefaultProfileImage()) + .reliability(new Reliability(0.0d)) .oauthId(userInformationDto.findUserId()) .build(); @@ -53,6 +76,25 @@ private User findOrPersistUser(final Oauth2Type oauth2Type, final UserInformatio }); } + private ProfileImage findDefaultProfileImage() { + return profileImageRepository.findByStoreName(DEFAULT_PROFILE_IMAGE_STORE_NAME) + .orElseThrow(() -> new ImageNotFoundException("기본 이미지를 찾을 수 없습니다.")); + } + + private String calculateRandomNumber() { + String name = RandomNameGenerator.generate(); + + while (isAlreadyExist(name)) { + name = RandomNameGenerator.generate(); + } + + return name; + } + + private boolean isAlreadyExist(final String name) { + return userRepository.existsByNameEndingWith(name); + } + private TokenDto convertTokenDto(final User persistUser) { final String accessToken = tokenEncoder.encode( LocalDateTime.now(), @@ -86,4 +128,24 @@ public boolean validateToken(final String accessToken) { return tokenDecoder.decode(TokenType.ACCESS, accessToken) .isPresent(); } + + @Transactional + public void withdrawal( + final Oauth2Type oauth2Type, + final String accessToken, + final String refreshToken + ) throws InvalidWithdrawalException { + final OAuth2UserInformationProvider provider = providerComposite.findProvider(oauth2Type); + final PrivateClaims privateClaims = tokenDecoder.decode(TokenType.ACCESS, accessToken) + .orElseThrow(() -> + new InvalidTokenException("유효한 토큰이 아닙니다.") + ); + final User user = userRepository.findByIdAndDeletedIsFalse(privateClaims.userId()) + .orElseThrow(() -> new InvalidWithdrawalException("탈퇴에 대한 권한 없습니다.")); + + user.withdrawal(); + blackListTokenService.registerBlackListToken(accessToken, refreshToken); + deviceTokenRepository.deleteByUserId(user.getId()); + provider.unlinkUserBy(user.getOauthId()); + } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/authentication/application/exception/InvalidWithdrawalException.java b/backend/ddang/src/main/java/com/ddang/ddang/authentication/application/exception/InvalidWithdrawalException.java new file mode 100644 index 000000000..55e4c693e --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/authentication/application/exception/InvalidWithdrawalException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.authentication.application.exception; + +public class InvalidWithdrawalException extends IllegalArgumentException { + + public InvalidWithdrawalException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/authentication/application/util/RandomNameGenerator.java b/backend/ddang/src/main/java/com/ddang/ddang/authentication/application/util/RandomNameGenerator.java new file mode 100644 index 000000000..d88a60158 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/authentication/application/util/RandomNameGenerator.java @@ -0,0 +1,24 @@ +package com.ddang.ddang.authentication.application.util; + +import java.util.Random; + +public class RandomNameGenerator { + + private static final int NAME_LENGTH = 10; + + private static final Random random = new Random(); + + private RandomNameGenerator() { + } + + public static String generate() { + StringBuilder name = new StringBuilder(); + + for (int i = 0; i < NAME_LENGTH; i++) { + int digit = random.nextInt(10); + name.append(digit); + } + + return name.toString(); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/authentication/configuration/KakaoProvidersConfigurationProperties.java b/backend/ddang/src/main/java/com/ddang/ddang/authentication/configuration/KakaoProvidersConfigurationProperties.java index 45c13c5b6..8810db2d0 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/authentication/configuration/KakaoProvidersConfigurationProperties.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/authentication/configuration/KakaoProvidersConfigurationProperties.java @@ -3,5 +3,5 @@ import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties("oauth2.client.providers.kakao") -public record KakaoProvidersConfigurationProperties(String userInfoUri) { +public record KakaoProvidersConfigurationProperties(String adminKey, String userInfoUri, String userUnlinkUri) { } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/authentication/infrastructure/jwt/JwtDecoder.java b/backend/ddang/src/main/java/com/ddang/ddang/authentication/infrastructure/jwt/JwtDecoder.java index 0f7f29d38..47dcadbd8 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/authentication/infrastructure/jwt/JwtDecoder.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/authentication/infrastructure/jwt/JwtDecoder.java @@ -36,7 +36,7 @@ private void validateBearerToken(final String token) { final String tokenType = token.substring(0, BEARER_END_INDEX); validateTokenType(tokenType); - } catch (final StringIndexOutOfBoundsException ex) { + } catch (final StringIndexOutOfBoundsException | NullPointerException ex) { throw new InvalidTokenException("Bearer 타입이 아니거나 유효한 토큰이 아닙니다.", ex); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/authentication/infrastructure/jwt/JwtEncoder.java b/backend/ddang/src/main/java/com/ddang/ddang/authentication/infrastructure/jwt/JwtEncoder.java index 24ec08f25..8c09a127c 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/authentication/infrastructure/jwt/JwtEncoder.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/authentication/infrastructure/jwt/JwtEncoder.java @@ -19,6 +19,8 @@ @RequiredArgsConstructor public class JwtEncoder implements TokenEncoder { + public static final String TOKEN_PREFIX = "Bearer "; + private final JwtConfigurationProperties jwtConfigurationProperties; @Override @@ -31,7 +33,7 @@ public String encode( final String key = jwtConfigurationProperties.findTokenKey(tokenType); final Long expiredHours = jwtConfigurationProperties.findExpiredHours(tokenType); - return Jwts.builder() + return TOKEN_PREFIX + Jwts.builder() .setIssuedAt(targetDate) .setExpiration(new Date(targetDate.getTime() + expiredHours * 60 * 60 * 1000L)) .addClaims(privateClaims) diff --git a/backend/ddang/src/main/java/com/ddang/ddang/authentication/infrastructure/oauth2/OAuth2UserInformationProvider.java b/backend/ddang/src/main/java/com/ddang/ddang/authentication/infrastructure/oauth2/OAuth2UserInformationProvider.java index 87996d889..278da9fac 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/authentication/infrastructure/oauth2/OAuth2UserInformationProvider.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/authentication/infrastructure/oauth2/OAuth2UserInformationProvider.java @@ -7,4 +7,6 @@ public interface OAuth2UserInformationProvider { Oauth2Type supportsOauth2Type(); UserInformationDto findUserInformation(final String accessToken); + + UserInformationDto unlinkUserBy(final String oauthId); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/authentication/infrastructure/oauth2/Oauth2Type.java b/backend/ddang/src/main/java/com/ddang/ddang/authentication/infrastructure/oauth2/Oauth2Type.java index 89da5853f..7b79ca69c 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/authentication/infrastructure/oauth2/Oauth2Type.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/authentication/infrastructure/oauth2/Oauth2Type.java @@ -1,7 +1,7 @@ package com.ddang.ddang.authentication.infrastructure.oauth2; -import com.ddang.ddang.authentication.domain.dto.UserInformationDto; import com.ddang.ddang.authentication.domain.exception.UnsupportedSocialLoginException; + import java.util.Locale; public enum Oauth2Type { @@ -16,9 +16,9 @@ public static Oauth2Type from(final String typeName) { } } - public String calculateNickname(final UserInformationDto dto) { - return this.name() - .toLowerCase(Locale.ENGLISH) - .concat(String.valueOf(dto.id())); + public String calculateNickname(final String name) { + final String type = this.name() + .toLowerCase(Locale.ENGLISH); + return type + name; } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/authentication/infrastructure/oauth2/kakao/KakaoUserInformationProvider.java b/backend/ddang/src/main/java/com/ddang/ddang/authentication/infrastructure/oauth2/kakao/KakaoUserInformationProvider.java index 2da4b1102..55f19afc2 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/authentication/infrastructure/oauth2/kakao/KakaoUserInformationProvider.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/authentication/infrastructure/oauth2/kakao/KakaoUserInformationProvider.java @@ -12,6 +12,8 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; @@ -20,6 +22,7 @@ public class KakaoUserInformationProvider implements OAuth2UserInformationProvider { private static final String TOKEN_TYPE = "Bearer "; + private static final String KAKAO_ADMIN_TOKEN_TYPE = "KakaoAK "; private static final String REST_TEMPLATE_MESSAGE_SEPARATOR = ":"; private static final int MESSAGE_INDEX = 0; @@ -54,4 +57,32 @@ public UserInformationDto findUserInformation(final String accessToken) { throw new InvalidTokenException(message, ex); } } + + @Override + public UserInformationDto unlinkUserBy(final String oauthId) { + final HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set(HttpHeaders.AUTHORIZATION, KAKAO_ADMIN_TOKEN_TYPE + providersConfigurationProperties.adminKey()); + + final MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("target_id_type", "user_id"); + body.add("target_id", oauthId); + + final HttpEntity> request = new HttpEntity<>(body, headers); + + try { + final ResponseEntity response = restTemplate.exchange( + providersConfigurationProperties.userUnlinkUri(), + HttpMethod.POST, + request, + UserInformationDto.class + ); + + return response.getBody(); + } catch (final HttpClientErrorException ex) { + final String message = ex.getMessage().split(REST_TEMPLATE_MESSAGE_SEPARATOR)[MESSAGE_INDEX]; + + throw new InvalidTokenException(message, ex); + } + } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/authentication/presentation/AuthenticationController.java b/backend/ddang/src/main/java/com/ddang/ddang/authentication/presentation/AuthenticationController.java index dc9fb9b2d..0e2b0eed7 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/authentication/presentation/AuthenticationController.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/authentication/presentation/AuthenticationController.java @@ -4,9 +4,10 @@ import com.ddang.ddang.authentication.application.BlackListTokenService; import com.ddang.ddang.authentication.application.dto.TokenDto; import com.ddang.ddang.authentication.infrastructure.oauth2.Oauth2Type; -import com.ddang.ddang.authentication.presentation.dto.request.AccessTokenRequest; +import com.ddang.ddang.authentication.presentation.dto.request.LoginTokenRequest; import com.ddang.ddang.authentication.presentation.dto.request.LogoutRequest; import com.ddang.ddang.authentication.presentation.dto.request.RefreshTokenRequest; +import com.ddang.ddang.authentication.presentation.dto.request.WithdrawalRequest; import com.ddang.ddang.authentication.presentation.dto.response.TokenResponse; import com.ddang.ddang.authentication.presentation.dto.response.ValidatedTokenResponse; import jakarta.validation.Valid; @@ -30,11 +31,11 @@ public class AuthenticationController { private final BlackListTokenService blackListTokenService; @PostMapping("/login/{oauth2Type}") - public ResponseEntity validate( + public ResponseEntity login( @PathVariable final Oauth2Type oauth2Type, - @RequestBody final AccessTokenRequest request + @RequestBody final LoginTokenRequest request ) { - final TokenDto tokenDto = authenticationService.login(oauth2Type, request.accessToken()); + final TokenDto tokenDto = authenticationService.login(oauth2Type, request.accessToken(), request.deviceToken()); return ResponseEntity.ok(TokenResponse.from(tokenDto)); } @@ -65,4 +66,16 @@ public ResponseEntity logout( return ResponseEntity.noContent() .build(); } + + @PostMapping("/withdrawal/{oauth2Type}") + public ResponseEntity withdrawal( + @PathVariable final Oauth2Type oauth2Type, + @RequestHeader(HttpHeaders.AUTHORIZATION) final String accessToken, + @RequestBody @Valid final WithdrawalRequest request + ) { + authenticationService.withdrawal(oauth2Type, accessToken, request.refreshToken()); + + return ResponseEntity.noContent() + .build(); + } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/authentication/presentation/dto/request/LoginTokenRequest.java b/backend/ddang/src/main/java/com/ddang/ddang/authentication/presentation/dto/request/LoginTokenRequest.java new file mode 100644 index 000000000..cd148e473 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/authentication/presentation/dto/request/LoginTokenRequest.java @@ -0,0 +1,11 @@ +package com.ddang.ddang.authentication.presentation.dto.request; + +import jakarta.validation.constraints.NotEmpty; + +public record LoginTokenRequest( + @NotEmpty(message = "AccessToken을 입력해주세요.") + String accessToken, + + String deviceToken +) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/authentication/presentation/dto/request/AccessTokenRequest.java b/backend/ddang/src/main/java/com/ddang/ddang/authentication/presentation/dto/request/WithdrawalRequest.java similarity index 50% rename from backend/ddang/src/main/java/com/ddang/ddang/authentication/presentation/dto/request/AccessTokenRequest.java rename to backend/ddang/src/main/java/com/ddang/ddang/authentication/presentation/dto/request/WithdrawalRequest.java index bd1452c86..df304fd9d 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/authentication/presentation/dto/request/AccessTokenRequest.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/authentication/presentation/dto/request/WithdrawalRequest.java @@ -2,5 +2,5 @@ import jakarta.validation.constraints.NotEmpty; -public record AccessTokenRequest(@NotEmpty(message = "AccessToken을 입력해주세요.") String accessToken) { +public record WithdrawalRequest(@NotEmpty(message = "refreshToken을 입력해주세요.") String refreshToken) { } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/bid/application/BidService.java b/backend/ddang/src/main/java/com/ddang/ddang/bid/application/BidService.java index 8a04a8b13..49e6375b5 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/bid/application/BidService.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/bid/application/BidService.java @@ -3,8 +3,11 @@ import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; import com.ddang.ddang.auction.domain.Auction; import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.auction.infrastructure.persistence.dto.AuctionAndImageDto; +import com.ddang.ddang.bid.application.dto.BidDto; import com.ddang.ddang.bid.application.dto.CreateBidDto; import com.ddang.ddang.bid.application.dto.ReadBidDto; +import com.ddang.ddang.bid.application.event.BidNotificationEvent; import com.ddang.ddang.bid.application.exception.InvalidAuctionToBidException; import com.ddang.ddang.bid.application.exception.InvalidBidPriceException; import com.ddang.ddang.bid.application.exception.InvalidBidderException; @@ -15,34 +18,64 @@ import com.ddang.ddang.user.domain.User; import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; @Service @Transactional(readOnly = true) @RequiredArgsConstructor +@Slf4j public class BidService { + private final ApplicationEventPublisher bidEventPublisher; private final JpaAuctionRepository auctionRepository; private final JpaUserRepository userRepository; private final JpaBidRepository bidRepository; @Transactional - public Long create(final CreateBidDto bidDto) { + public Long create(final CreateBidDto bidDto, final String auctionImageAbsoluteUrl) { final User bidder = userRepository.findById(bidDto.userId()) .orElseThrow(() -> new UserNotFoundException("해당 사용자를 찾을 수 없습니다.")); - final Auction auction = auctionRepository.findById(bidDto.auctionId()) - .orElseThrow(() -> new AuctionNotFoundException("해당 경매를 찾을 수 없습니다.")); + final AuctionAndImageDto auctionAndImageDto = + auctionRepository.findDtoByAuctionId(bidDto.auctionId()) + .orElseThrow(() -> new AuctionNotFoundException("해당 경매를 찾을 수 없습니다.")); + + final Auction auction = auctionAndImageDto.auction(); checkInvalidAuction(auction); checkInvalidBid(auction, bidder, bidDto); - final Bid saveBid = saveBid(bidDto, auction, bidder); + final Optional previousBidder = auction.findLastBidder(); + + final Bid saveBid = saveAndUpdateLastBid(bidDto, auction, bidder); + + publishBidNotificationEvent(auctionImageAbsoluteUrl, auctionAndImageDto, previousBidder); + return saveBid.getId(); } + private void publishBidNotificationEvent( + final String auctionImageAbsoluteUrl, + final AuctionAndImageDto auctionAndImageDto, + final Optional previousBidder + ) { + if (previousBidder.isEmpty()) { + return; + } + + final BidDto bidDto = new BidDto( + previousBidder.get().getId(), + auctionAndImageDto, + auctionImageAbsoluteUrl + ); + bidEventPublisher.publishEvent(new BidNotificationEvent(bidDto)); + } + private void checkInvalidAuction(final Auction auction) { final LocalDateTime now = LocalDateTime.now(); if (auction.isClosed(now)) { @@ -100,7 +133,7 @@ private void checkInvalidBidPrice(final Bid lastBid, final BidPrice bidPrice) { } } - private Bid saveBid(final CreateBidDto bidDto, final Auction auction, final User bidder) { + private Bid saveAndUpdateLastBid(final CreateBidDto bidDto, final Auction auction, final User bidder) { final Bid createBid = bidDto.toEntity(auction, bidder); final Bid saveBid = bidRepository.save(createBid); @@ -111,7 +144,7 @@ private Bid saveBid(final CreateBidDto bidDto, final Auction auction, final User public List readAllByAuctionId(final Long auctionId) { if (auctionRepository.existsById(auctionId)) { - final List bids = bidRepository.findByAuctionId(auctionId); + final List bids = bidRepository.findByAuctionIdOrderByIdAsc(auctionId); return bids.stream() .map(ReadBidDto::from) diff --git a/backend/ddang/src/main/java/com/ddang/ddang/bid/application/dto/BidDto.java b/backend/ddang/src/main/java/com/ddang/ddang/bid/application/dto/BidDto.java new file mode 100644 index 000000000..8787a5cf4 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/bid/application/dto/BidDto.java @@ -0,0 +1,10 @@ +package com.ddang.ddang.bid.application.dto; + +import com.ddang.ddang.auction.infrastructure.persistence.dto.AuctionAndImageDto; + +public record BidDto( + Long previousBidderId, + AuctionAndImageDto auctionAndImageDto, + String auctionImageAbsoluteUrl +) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/bid/application/dto/ReadBidDto.java b/backend/ddang/src/main/java/com/ddang/ddang/bid/application/dto/ReadBidDto.java index 20f2d1a1a..d4cf89c82 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/bid/application/dto/ReadBidDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/bid/application/dto/ReadBidDto.java @@ -1,13 +1,15 @@ package com.ddang.ddang.bid.application.dto; import com.ddang.ddang.bid.domain.Bid; +import com.ddang.ddang.image.application.util.ImageIdProcessor; import com.ddang.ddang.user.domain.User; import java.time.LocalDateTime; public record ReadBidDto( String name, - String profileImage, + Long profileImageId, + boolean isDeletedUser, int price, LocalDateTime bidTime ) { @@ -17,7 +19,8 @@ public static ReadBidDto from(final Bid bid) { return new ReadBidDto( bidder.getName(), - bidder.getProfileImage(), + ImageIdProcessor.process(bidder.getProfileImage()), + bidder.isDeleted(), bid.getPrice().getValue(), bid.getCreatedTime() ); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/bid/application/event/BidNotificationEvent.java b/backend/ddang/src/main/java/com/ddang/ddang/bid/application/event/BidNotificationEvent.java new file mode 100644 index 000000000..23e8786b9 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/bid/application/event/BidNotificationEvent.java @@ -0,0 +1,6 @@ +package com.ddang.ddang.bid.application.event; + +import com.ddang.ddang.bid.application.dto.BidDto; + +public record BidNotificationEvent(BidDto bidDto) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/bid/application/exception/InvalidBidException.java b/backend/ddang/src/main/java/com/ddang/ddang/bid/application/exception/InvalidBidException.java index cc77956e7..6b1e4cdbf 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/bid/application/exception/InvalidBidException.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/bid/application/exception/InvalidBidException.java @@ -1,6 +1,5 @@ package com.ddang.ddang.bid.application.exception; -// TODO: 2023/07/30 [고민] 상위 excpetion 클래스 위치는 어디가 좋은가? public class InvalidBidException extends IllegalArgumentException { public InvalidBidException(final String message) { diff --git a/backend/ddang/src/main/java/com/ddang/ddang/bid/infrastructure/persistence/JpaBidRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/bid/infrastructure/persistence/JpaBidRepository.java index 2f927c27e..7d12de1de 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/bid/infrastructure/persistence/JpaBidRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/bid/infrastructure/persistence/JpaBidRepository.java @@ -9,7 +9,7 @@ public interface JpaBidRepository extends JpaRepository { - List findByAuctionId(final Long id); + List findByAuctionIdOrderByIdAsc(final Long id); @Query("select b from Bid b where b.auction.id = :auctionId order by b.id desc limit 1") Bid findLastBidByAuctionId(@Param("auctionId") final Long id); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/bid/presentation/BidController.java b/backend/ddang/src/main/java/com/ddang/ddang/bid/presentation/BidController.java index 0c79f6ab4..89cba81ba 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/bid/presentation/BidController.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/bid/presentation/BidController.java @@ -8,6 +8,7 @@ import com.ddang.ddang.bid.presentation.dto.request.CreateBidRequest; import com.ddang.ddang.bid.presentation.dto.response.ReadBidResponse; import com.ddang.ddang.bid.presentation.dto.response.ReadBidsResponse; +import com.ddang.ddang.image.presentation.util.ImageRelativeUrl; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -33,7 +34,7 @@ public ResponseEntity create( @AuthenticateUser AuthenticationUserInfo userInfo, @RequestBody @Valid final CreateBidRequest bidRequest ) { - bidService.create(CreateBidDto.of(bidRequest, userInfo.userId())); + bidService.create(CreateBidDto.of(bidRequest, userInfo.userId()), ImageRelativeUrl.AUCTION.calculateAbsoluteUrl()); return ResponseEntity.created(URI.create("/auctions/" + bidRequest.auctionId())) .build(); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/bid/presentation/dto/response/ReadBidResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/bid/presentation/dto/response/ReadBidResponse.java index 8a21835ae..6f75d741f 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/bid/presentation/dto/response/ReadBidResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/bid/presentation/dto/response/ReadBidResponse.java @@ -1,6 +1,9 @@ package com.ddang.ddang.bid.presentation.dto.response; import com.ddang.ddang.bid.application.dto.ReadBidDto; +import com.ddang.ddang.image.presentation.util.ImageRelativeUrl; +import com.ddang.ddang.image.presentation.util.ImageUrlCalculator; +import com.ddang.ddang.user.presentation.util.NameProcessor; import com.fasterxml.jackson.annotation.JsonFormat; import java.time.LocalDateTime; @@ -17,6 +20,12 @@ public record ReadBidResponse( ) { public static ReadBidResponse from(final ReadBidDto dto) { - return new ReadBidResponse(dto.name(), dto.profileImage(), dto.price(), dto.bidTime()); + final String name = NameProcessor.process(dto.isDeletedUser(), dto.name()); + return new ReadBidResponse( + name, + ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, dto.profileImageId()), + dto.price(), + dto.bidTime() + ); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/category/presentation/CategoryController.java b/backend/ddang/src/main/java/com/ddang/ddang/category/presentation/CategoryController.java index a0a8de4bd..bda883ebe 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/category/presentation/CategoryController.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/category/presentation/CategoryController.java @@ -5,6 +5,7 @@ import com.ddang.ddang.category.presentation.dto.response.ReadCategoryResponse; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -19,6 +20,7 @@ public class CategoryController { private final CategoryService categoryService; @GetMapping + @Cacheable(cacheNames = "mainCategory") public ResponseEntity> readAllMain() { final List readCategoryDtos = categoryService.readAllMain(); final List readCategoryResponses = readCategoryDtos.stream() @@ -29,6 +31,7 @@ public ResponseEntity> readAllMain() { } @GetMapping("/{mainId}") + @Cacheable(cacheNames = "subCategory") public ResponseEntity > readAllSub(@PathVariable final Long mainId) { final List readCategoryDtos = categoryService.readAllSubByMainId(mainId); final List readCategoryResponses = readCategoryDtos.stream() diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/ChatRoomService.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/ChatRoomService.java index 5f04a78e8..d723e9f84 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/ChatRoomService.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/ChatRoomService.java @@ -1,18 +1,23 @@ package com.ddang.ddang.chat.application; +import com.ddang.ddang.auction.application.dto.ReadChatRoomDto; import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; import com.ddang.ddang.auction.domain.Auction; import com.ddang.ddang.auction.domain.exception.WinnerNotFoundException; import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.authentication.domain.dto.AuthenticationUserInfo; import com.ddang.ddang.chat.application.dto.CreateChatRoomDto; import com.ddang.ddang.chat.application.dto.ReadChatRoomWithLastMessageDto; import com.ddang.ddang.chat.application.dto.ReadParticipatingChatRoomDto; import com.ddang.ddang.chat.application.exception.ChatRoomNotFoundException; import com.ddang.ddang.chat.application.exception.InvalidAuctionToChatException; -import com.ddang.ddang.chat.application.exception.UserNotAccessibleException; +import com.ddang.ddang.chat.application.exception.InvalidUserToChat; import com.ddang.ddang.chat.domain.ChatRoom; import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; -import com.ddang.ddang.chat.infrastructure.persistence.JpaMessageRepository; +import com.ddang.ddang.chat.infrastructure.persistence.QuerydslChatRoomAndImageRepositoryImpl; +import com.ddang.ddang.chat.infrastructure.persistence.QuerydslChatRoomAndMessageAndImageRepository; +import com.ddang.ddang.chat.infrastructure.persistence.dto.ChatRoomAndImageDto; +import com.ddang.ddang.chat.infrastructure.persistence.dto.ChatRoomAndMessageAndImageDto; import com.ddang.ddang.user.application.exception.UserNotFoundException; import com.ddang.ddang.user.domain.User; import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; @@ -21,18 +26,18 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; -import static java.util.Comparator.comparing; - @Service @Transactional(readOnly = true) @RequiredArgsConstructor public class ChatRoomService { + private static final Long DEFAULT_CHAT_ROOM_ID = null; + private final JpaChatRoomRepository chatRoomRepository; - private final JpaMessageRepository messageRepository; + private final QuerydslChatRoomAndImageRepositoryImpl querydslChatRoomAndImageRepository; + private final QuerydslChatRoomAndMessageAndImageRepository querydslChatRoomAndMessageAndImageRepository; private final JpaUserRepository userRepository; private final JpaAuctionRepository auctionRepository; @@ -40,21 +45,18 @@ public class ChatRoomService { public Long create(final Long userId, final CreateChatRoomDto chatRoomDto) { final User findUser = userRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException("사용자 정보를 찾을 수 없습니다.")); - final Auction findAuction = auctionRepository.findById(chatRoomDto.auctionId()) + final Auction findAuction = auctionRepository.findAuctionById(chatRoomDto.auctionId()) .orElseThrow(() -> - new AuctionNotFoundException("해당 경매를 찾을 수 없습니다.")); - - final ChatRoom persistChatRoom = findOrCreateChatRoomByAuction(findUser, findAuction); - - return persistChatRoom.getId(); - } + new AuctionNotFoundException("해당 경매를 찾을 수 없습니다.") + ); - private ChatRoom findOrCreateChatRoomByAuction(final User user, final Auction auction) { - return chatRoomRepository.findByAuctionId(auction.getId()) - .orElseGet(() -> createAndSaveChatRoom(user, auction)); + return chatRoomRepository.findChatRoomIdByAuctionId(findAuction.getId()) + .orElseGet(() -> + persistChatRoom(findUser, findAuction).getId() + ); } - private ChatRoom createAndSaveChatRoom(final User user, final Auction auction) { + private ChatRoom persistChatRoom(final User user, final Auction auction) { checkAuctionStatus(auction); final User winner = auction.findWinner(LocalDateTime.now()) .orElseThrow(() -> new WinnerNotFoundException("낙찰자가 존재하지 않습니다")); @@ -69,62 +71,62 @@ private void checkAuctionStatus(final Auction findAuction) { if (!findAuction.isClosed(LocalDateTime.now())) { throw new InvalidAuctionToChatException("경매가 아직 종료되지 않았습니다."); } - if (findAuction.isDeleted()) { - throw new InvalidAuctionToChatException("삭제된 경매입니다."); - } } private void checkUserCanParticipate(final User findUser, final Auction findAuction) { - if (!isSellerOrWinner(findUser, findAuction)) { - throw new UserNotAccessibleException("경매의 판매자 또는 최종 낙찰자만 채팅이 가능합니다."); + if (isNotSellerAndNotWinner(findUser, findAuction)) { + throw new InvalidUserToChat("경매의 판매자 또는 최종 낙찰자만 채팅이 가능합니다."); } } - private boolean isSellerOrWinner(final User findUser, final Auction findAuction) { - return findAuction.isOwner(findUser) || findAuction.isWinner(findUser, LocalDateTime.now()); + private boolean isNotSellerAndNotWinner(final User findUser, final Auction findAuction) { + return !(findAuction.isOwner(findUser) || findAuction.isWinner(findUser, LocalDateTime.now())); } public List readAllByUserId(final Long userId) { final User findUser = userRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException("사용자 정보를 찾을 수 없습니다.")); - final List chatRooms = chatRoomRepository.findAllByUserId(findUser.getId()); + final List chatRoomAndMessageAndImageQueryProjectionDtos = + querydslChatRoomAndMessageAndImageRepository.findAllChatRoomInfoByUserIdOrderByLastMessage(findUser.getId()); - return processChatRoomWithLastMessageAndSort(findUser, chatRooms); - } - - private List processChatRoomWithLastMessageAndSort( - final User findUser, - final List chatRooms - ) { - final List chatRoomDtos = new ArrayList<>(); - for (final ChatRoom chatRoom : chatRooms) { - messageRepository.findLastMessageByChatRoomId(chatRoom.getId()) - .ifPresent(message -> - chatRoomDtos.add(ReadChatRoomWithLastMessageDto.of(findUser, chatRoom, message)) - ); - } - - return chatRoomDtos.stream() - .sorted(comparing((ReadChatRoomWithLastMessageDto dto) -> dto.lastMessageDto().createdTime()) - .reversed()) - .toList(); + return chatRoomAndMessageAndImageQueryProjectionDtos.stream() + .map(dto -> ReadChatRoomWithLastMessageDto.of(findUser, dto)) + .toList(); } public ReadParticipatingChatRoomDto readByChatRoomId(final Long chatRoomId, final Long userId) { final User findUser = userRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException("사용자 정보를 찾을 수 없습니다.")); - final ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId) - .orElseThrow(() -> new ChatRoomNotFoundException( - "지정한 아이디에 대한 채팅방을 찾을 수 없습니다." - )); - checkAccessible(findUser, chatRoom); - - return ReadParticipatingChatRoomDto.of(findUser, chatRoom, LocalDateTime.now()); + final ChatRoomAndImageDto chatRoomAndImageDto = + querydslChatRoomAndImageRepository.findChatRoomById(chatRoomId) + .orElseThrow(() -> new ChatRoomNotFoundException( + "지정한 아이디에 대한 채팅방을 찾을 수 없습니다." + )); + checkAccessible(findUser, chatRoomAndImageDto.chatRoom()); + + return ReadParticipatingChatRoomDto.of(findUser, chatRoomAndImageDto); } private void checkAccessible(final User findUser, final ChatRoom chatRoom) { if (!chatRoom.isParticipant(findUser)) { - throw new UserNotAccessibleException("해당 채팅방에 접근할 권한이 없습니다."); + throw new InvalidUserToChat("해당 채팅방에 접근할 권한이 없습니다."); } } + + public ReadChatRoomDto readChatInfoByAuctionId(final Long auctionId, final AuthenticationUserInfo userInfo) { + final User findUser = userRepository.findById(userInfo.userId()) + .orElseThrow(() -> new UserNotFoundException("회원 정보를 찾을 수 없습니다.")); + final Auction findAuction = auctionRepository.findAuctionById(auctionId) + .orElseThrow(() -> new AuctionNotFoundException( + "지정한 아이디에 대한 경매를 찾을 수 없습니다." + )); + final Long chatRoomId = chatRoomRepository.findChatRoomIdByAuctionId(findAuction.getId()) + .orElse(DEFAULT_CHAT_ROOM_ID); + + return new ReadChatRoomDto(chatRoomId, isChatParticipant(findAuction, findUser)); + } + + private boolean isChatParticipant(final Auction findAuction, final User findUser) { + return findAuction.isClosed(LocalDateTime.now()) && findAuction.isSellerOrWinner(findUser, LocalDateTime.now()); + } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/MessageService.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/MessageService.java index f52af5015..7b26fc486 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/MessageService.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/MessageService.java @@ -1,7 +1,9 @@ package com.ddang.ddang.chat.application; import com.ddang.ddang.chat.application.dto.CreateMessageDto; +import com.ddang.ddang.chat.application.dto.MessageDto; import com.ddang.ddang.chat.application.dto.ReadMessageDto; +import com.ddang.ddang.chat.application.event.MessageNotificationEvent; import com.ddang.ddang.chat.application.exception.ChatRoomNotFoundException; import com.ddang.ddang.chat.application.exception.MessageNotFoundException; import com.ddang.ddang.chat.application.exception.UnableToChatException; @@ -14,24 +16,26 @@ import com.ddang.ddang.user.domain.User; import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; import java.util.List; -import java.util.stream.Collectors; @Service @Transactional(readOnly = true) @RequiredArgsConstructor +@Slf4j public class MessageService { + private final ApplicationEventPublisher messageEventPublisher; private final JpaMessageRepository messageRepository; private final JpaChatRoomRepository chatRoomRepository; private final JpaUserRepository userRepository; @Transactional - public Long create(final CreateMessageDto dto) { + public Long create(final CreateMessageDto dto, final String profileImageAbsoluteUrl) { final ChatRoom chatRoom = chatRoomRepository.findById(dto.chatRoomId()) .orElseThrow(() -> new ChatRoomNotFoundException( "지정한 아이디에 대한 채팅방을 찾을 수 없습니다.")); @@ -42,22 +46,25 @@ public Long create(final CreateMessageDto dto) { .orElseThrow(() -> new UserNotFoundException( "지정한 아이디에 대한 수신자를 찾을 수 없습니다.")); - if (!chatRoom.isChatAvailableTime(LocalDateTime.now())) { - throw new UnableToChatException("현재 메시지 전송이 불가능합니다."); + if (!chatRoom.isChatAvailablePartner(receiver)) { + throw new UnableToChatException("탈퇴한 사용자에게는 메시지 전송이 불가능합니다."); } final Message message = dto.toEntity(chatRoom, writer, receiver); final Message persistMessage = messageRepository.save(message); + final MessageDto messageDto = MessageDto.of(persistMessage, chatRoom, writer, receiver, profileImageAbsoluteUrl); + messageEventPublisher.publishEvent(new MessageNotificationEvent(messageDto)); return persistMessage.getId(); } public List readAllByLastMessageId(final ReadMessageRequest request) { - final User user = userRepository.findById(request.userId()) - .orElseThrow(() -> new UserNotFoundException( - "지정한 아이디에 대한 사용자를 찾을 수 없습니다.")); + if (!userRepository.existsById(request.messageReaderId())) { + throw new UserNotFoundException("지정한 아이디에 대한 사용자를 찾을 수 없습니다."); + } + final ChatRoom chatRoom = chatRoomRepository.findById(request.chatRoomId()) .orElseThrow(() -> new ChatRoomNotFoundException( "지정한 아이디에 대한 채팅방을 찾을 수 없습니다.")); @@ -67,14 +74,14 @@ public List readAllByLastMessageId(final ReadMessageRequest requ } final List readMessages = messageRepository.findMessagesAllByLastMessageId( - user.getId(), + request.messageReaderId(), chatRoom.getId(), request.lastMessageId() ); return readMessages.stream() - .map(ReadMessageDto::from) - .collect(Collectors.toList()); + .map(message -> ReadMessageDto.from(message, chatRoom)) + .toList(); } private void validateLastMessageId(final Long lastMessageId) { diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ChatRoomInMessageDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ChatRoomInMessageDto.java new file mode 100644 index 000000000..7dab049c4 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ChatRoomInMessageDto.java @@ -0,0 +1,14 @@ +package com.ddang.ddang.chat.application.dto; + +import com.ddang.ddang.chat.domain.ChatRoom; + +public record ChatRoomInMessageDto(Long id, Long auctionId, Long buyerId) { + + public static ChatRoomInMessageDto from(final ChatRoom chatRoom) { + return new ChatRoomInMessageDto( + chatRoom.getId(), + chatRoom.getAuction().getId(), + chatRoom.getBuyer().getId() + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/MessageDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/MessageDto.java new file mode 100644 index 000000000..ce2feed4f --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/MessageDto.java @@ -0,0 +1,36 @@ +package com.ddang.ddang.chat.application.dto; + +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.user.domain.User; + +import java.time.LocalDateTime; + +public record MessageDto( + Long id, + LocalDateTime createdTime, + ChatRoom chatRoom, + User writer, + User receiver, + String contents, + String profileImageAbsoluteUrl +) { + + public static MessageDto of( + final Message persistMessage, + final ChatRoom chatRoom, + final User writer, + final User receiver, + final String profileImageAbsoluteUrl + ) { + return new MessageDto( + persistMessage.getId(), + persistMessage.getCreatedTime(), + chatRoom, + writer, + receiver, + persistMessage.getContents(), + profileImageAbsoluteUrl + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadAuctionInChatRoomDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadAuctionInChatRoomDto.java index 6e9e09fc2..233215c83 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadAuctionInChatRoomDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadAuctionInChatRoomDto.java @@ -4,43 +4,22 @@ import com.ddang.ddang.bid.domain.Bid; import com.ddang.ddang.image.domain.AuctionImage; -import java.util.List; - public record ReadAuctionInChatRoomDto( Long id, String title, Integer lastBidPrice, - List auctionImageIds, - String mainCategory, - String subCategory, - Long sellerId, - String sellerProfile, - String sellerName, - double sellerReliability + Long thumbnailImageId ) { - public static ReadAuctionInChatRoomDto from(final Auction auction) { + public static ReadAuctionInChatRoomDto of(final Auction auction, final AuctionImage thumbnailImage) { return new ReadAuctionInChatRoomDto( auction.getId(), auction.getTitle(), convertPrice(auction.getLastBid()), - convertImageUrls(auction), - auction.getSubCategory().getMainCategory().getName(), - auction.getSubCategory().getName(), - auction.getSeller().getId(), - auction.getSeller().getProfileImage(), - auction.getSeller().getName(), - auction.getSeller().getReliability() + thumbnailImage.getId() ); } - private static List convertImageUrls(final Auction auction) { - return auction.getAuctionImages() - .stream() - .map(AuctionImage::getId) - .toList(); - } - private static Integer convertPrice(final Bid bid) { if (bid == null) { return null; diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadChatRoomWithLastMessageDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadChatRoomWithLastMessageDto.java index 859a5894e..012b60754 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadChatRoomWithLastMessageDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadChatRoomWithLastMessageDto.java @@ -2,10 +2,10 @@ import com.ddang.ddang.chat.domain.ChatRoom; import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.chat.infrastructure.persistence.dto.ChatRoomAndMessageAndImageDto; +import com.ddang.ddang.image.domain.AuctionImage; import com.ddang.ddang.user.domain.User; -import java.time.LocalDateTime; - public record ReadChatRoomWithLastMessageDto( Long id, ReadAuctionInChatRoomDto auctionDto, @@ -16,17 +16,19 @@ public record ReadChatRoomWithLastMessageDto( public static ReadChatRoomWithLastMessageDto of( final User findUser, - final ChatRoom chatRoom, - final Message lastMessage + final ChatRoomAndMessageAndImageDto chatRoomAndMessageAndImageDto ) { + final ChatRoom chatRoom = chatRoomAndMessageAndImageDto.chatRoom(); final User partner = chatRoom.calculateChatPartnerOf(findUser); + final Message lastMessage = chatRoomAndMessageAndImageDto.message(); + final AuctionImage thumbnailImage = chatRoomAndMessageAndImageDto.thumbnailImage(); return new ReadChatRoomWithLastMessageDto( chatRoom.getId(), - ReadAuctionInChatRoomDto.from(chatRoom.getAuction()), + ReadAuctionInChatRoomDto.of(chatRoom.getAuction(), thumbnailImage), ReadUserInChatRoomDto.from(partner), ReadLastMessageDto.from(lastMessage), - chatRoom.isChatAvailableTime(LocalDateTime.now()) + chatRoom.isChatAvailablePartner(partner) ); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadMessageDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadMessageDto.java index 7837abae3..c6050ec9b 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadMessageDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadMessageDto.java @@ -2,38 +2,29 @@ import com.ddang.ddang.chat.domain.ChatRoom; import com.ddang.ddang.chat.domain.Message; -import com.ddang.ddang.user.domain.User; import java.time.LocalDateTime; public record ReadMessageDto( Long id, LocalDateTime createdTime, - ReadParticipatingChatRoomDto chatRoomDto, - ReadUserInChatRoomDto writerDto, - ReadUserInChatRoomDto receiverDto, + Long chatRoomId, + Long writerId, + Long receiverId, String contents ) { - public static ReadMessageDto from(final Message message) { + public static ReadMessageDto from( + final Message message, + final ChatRoom chatRoom + ) { return new ReadMessageDto( message.getId(), message.getCreatedTime(), - toReadParticipatingChatRoomDto(message.getChatRoom(), message.getWriter()), - ReadUserInChatRoomDto.from(message.getWriter()), - ReadUserInChatRoomDto.from(message.getReceiver()), + chatRoom.getId(), + message.getWriter().getId(), + message.getReceiver().getId(), message.getContents() ); } - - private static ReadParticipatingChatRoomDto toReadParticipatingChatRoomDto( - final ChatRoom chatRoom, - final User writer - ) { - return ReadParticipatingChatRoomDto.of( - chatRoom.calculateChatPartnerOf(writer), - chatRoom, - chatRoom.getCreatedTime() - ); - } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadParticipatingChatRoomDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadParticipatingChatRoomDto.java index 06050c729..318040fc1 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadParticipatingChatRoomDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadParticipatingChatRoomDto.java @@ -1,10 +1,9 @@ package com.ddang.ddang.chat.application.dto; import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.infrastructure.persistence.dto.ChatRoomAndImageDto; import com.ddang.ddang.user.domain.User; -import java.time.LocalDateTime; - public record ReadParticipatingChatRoomDto( Long id, ReadAuctionInChatRoomDto auctionDto, @@ -14,16 +13,16 @@ public record ReadParticipatingChatRoomDto( public static ReadParticipatingChatRoomDto of( final User findUser, - final ChatRoom chatRoom, - final LocalDateTime targetTime + final ChatRoomAndImageDto chatRoomAndImageDto ) { + final ChatRoom chatRoom = chatRoomAndImageDto.chatRoom(); final User partner = chatRoom.calculateChatPartnerOf(findUser); return new ReadParticipatingChatRoomDto( chatRoom.getId(), - ReadAuctionInChatRoomDto.from(chatRoom.getAuction()), + ReadAuctionInChatRoomDto.of(chatRoom.getAuction(), chatRoomAndImageDto.thumbnailImage()), ReadUserInChatRoomDto.from(partner), - chatRoom.isChatAvailableTime(targetTime) + chatRoom.isChatAvailablePartner(partner) ); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadUserInChatRoomDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadUserInChatRoomDto.java index 16fa0f087..604827146 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadUserInChatRoomDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadUserInChatRoomDto.java @@ -1,10 +1,17 @@ package com.ddang.ddang.chat.application.dto; +import com.ddang.ddang.image.application.util.ImageIdProcessor; import com.ddang.ddang.user.domain.User; -public record ReadUserInChatRoomDto(Long id, String name, String profileImage, double reliability) { +public record ReadUserInChatRoomDto(Long id, String name, Long profileImageId, double reliability, boolean isDeleted) { public static ReadUserInChatRoomDto from(final User user) { - return new ReadUserInChatRoomDto(user.getId(), user.getName(), user.getProfileImage(), user.getReliability()); + return new ReadUserInChatRoomDto( + user.getId(), + user.getName(), + ImageIdProcessor.process(user.getProfileImage()), + user.getReliability().getValue(), + user.isDeleted() + ); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/event/MessageNotificationEvent.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/event/MessageNotificationEvent.java new file mode 100644 index 000000000..56bf90d60 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/event/MessageNotificationEvent.java @@ -0,0 +1,6 @@ +package com.ddang.ddang.chat.application.event; + +import com.ddang.ddang.chat.application.dto.MessageDto; + +public record MessageNotificationEvent(MessageDto messageDto) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/exception/InvalidUserToChat.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/exception/InvalidUserToChat.java new file mode 100644 index 000000000..f829af855 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/exception/InvalidUserToChat.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.chat.application.exception; + +public class InvalidUserToChat extends IllegalStateException { + + public InvalidUserToChat(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/exception/UserNotAccessibleException.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/exception/UserNotAccessibleException.java deleted file mode 100644 index 60d529bd3..000000000 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/exception/UserNotAccessibleException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.ddang.ddang.chat.application.exception; - -public class UserNotAccessibleException extends IllegalStateException { - - public UserNotAccessibleException(final String message) { - super(message); - } -} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/ChatRoom.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/ChatRoom.java index d11a1033b..60e6b2dc8 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/ChatRoom.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/ChatRoom.java @@ -18,12 +18,10 @@ import lombok.NoArgsConstructor; import lombok.ToString; -import java.time.LocalDateTime; - @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter -@EqualsAndHashCode(of = "id") +@EqualsAndHashCode(of = "id", callSuper = false) @ToString(of = "id") public class ChatRoom extends BaseCreateTimeEntity { @@ -46,10 +44,8 @@ public ChatRoom(final Auction auction, final User buyer) { this.buyer = buyer; } - public boolean isChatAvailableTime(final LocalDateTime targetTime) { - final LocalDateTime maxChatTime = getCreatedTime().plusDays(CHAT_EXPIRATION_DAY); - - return targetTime.isBefore(maxChatTime); + public boolean isChatAvailablePartner(final User partner) { + return !partner.isDeleted(); } public User calculateChatPartnerOf(final User user) { diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndImageRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndImageRepository.java new file mode 100644 index 000000000..66aa1122b --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndImageRepository.java @@ -0,0 +1,10 @@ +package com.ddang.ddang.chat.infrastructure.persistence; + +import com.ddang.ddang.chat.infrastructure.persistence.dto.ChatRoomAndImageDto; + +import java.util.Optional; + +public interface QuerydslChatRoomAndImageRepository { + + Optional findChatRoomById(final Long chatRoomId); +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndImageRepositoryImpl.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndImageRepositoryImpl.java new file mode 100644 index 000000000..89ce4d1e6 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndImageRepositoryImpl.java @@ -0,0 +1,49 @@ +package com.ddang.ddang.chat.infrastructure.persistence; + +import com.ddang.ddang.chat.infrastructure.persistence.dto.ChatRoomAndImageDto; +import com.ddang.ddang.chat.infrastructure.persistence.dto.ChatRoomAndImageQueryProjectionDto; +import com.ddang.ddang.chat.infrastructure.persistence.dto.QChatRoomAndImageQueryProjectionDto; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +import static com.ddang.ddang.auction.domain.QAuction.auction; +import static com.ddang.ddang.chat.domain.QChatRoom.chatRoom; +import static com.ddang.ddang.image.domain.QAuctionImage.auctionImage; + +@Repository +@RequiredArgsConstructor +public class QuerydslChatRoomAndImageRepositoryImpl implements QuerydslChatRoomAndImageRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public Optional findChatRoomById(final Long chatRoomId) { + + final ChatRoomAndImageQueryProjectionDto chatRoomAndImageQueryProjectionDto = + queryFactory.select(new QChatRoomAndImageQueryProjectionDto(chatRoom, auctionImage)) + .from(chatRoom) + .leftJoin(chatRoom.buyer).fetchJoin() + .leftJoin(chatRoom.auction, auction).fetchJoin() + .leftJoin(auction.seller).fetchJoin() + .leftJoin(auctionImage).on(auctionImage.id.eq( + JPAExpressions + .select(auctionImage.id.min()) + .from(auctionImage) + .where(auctionImage.auction.id.eq(auction.id)) + .groupBy(auctionImage.auction.id) + )).fetchJoin() + .leftJoin(auction.lastBid).fetchJoin() + .where(chatRoom.id.eq(chatRoomId)) + .fetchOne(); + + if (chatRoomAndImageQueryProjectionDto == null) { + return Optional.empty(); + } + + return Optional.of(chatRoomAndImageQueryProjectionDto.toDto()); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepository.java new file mode 100644 index 000000000..9a5cc8092 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepository.java @@ -0,0 +1,10 @@ +package com.ddang.ddang.chat.infrastructure.persistence; + +import com.ddang.ddang.chat.infrastructure.persistence.dto.ChatRoomAndMessageAndImageDto; + +import java.util.List; + +public interface QuerydslChatRoomAndMessageAndImageRepository { + + List findAllChatRoomInfoByUserIdOrderByLastMessage(final Long userId); +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepositoryImpl.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepositoryImpl.java new file mode 100644 index 000000000..6dd6c8509 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepositoryImpl.java @@ -0,0 +1,74 @@ +package com.ddang.ddang.chat.infrastructure.persistence; + +import com.ddang.ddang.chat.infrastructure.persistence.dto.ChatRoomAndMessageAndImageDto; +import com.ddang.ddang.chat.infrastructure.persistence.dto.ChatRoomAndMessageAndImageQueryProjectionDto; +import com.ddang.ddang.chat.infrastructure.persistence.dto.QChatRoomAndMessageAndImageQueryProjectionDto; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Objects; + +import static com.ddang.ddang.auction.domain.QAuction.auction; +import static com.ddang.ddang.chat.domain.QChatRoom.chatRoom; +import static com.ddang.ddang.chat.domain.QMessage.message; +import static com.ddang.ddang.image.domain.QAuctionImage.auctionImage; +import static java.util.Comparator.comparing; + +@Repository +@RequiredArgsConstructor +public class QuerydslChatRoomAndMessageAndImageRepositoryImpl implements QuerydslChatRoomAndMessageAndImageRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public List findAllChatRoomInfoByUserIdOrderByLastMessage(final Long userId) { + final List unsortedDtos = + queryFactory.select(new QChatRoomAndMessageAndImageQueryProjectionDto(chatRoom, message, auctionImage)) + .from(chatRoom) + .leftJoin(chatRoom.buyer).fetchJoin() + .leftJoin(chatRoom.auction, auction).fetchJoin() + .leftJoin(auction.seller).fetchJoin() + .leftJoin(auctionImage).on(auctionImage.id.eq( + JPAExpressions + .select(auctionImage.id.min()) + .from(auctionImage) + .where(auctionImage.auction.id.eq(auction.id)) + .groupBy(auctionImage.auction.id) + )).fetchJoin() + .leftJoin(auction.lastBid).fetchJoin() + .leftJoin(message).on(message.id.eq( + JPAExpressions + .select(message.id.max()) + .from(message) + .where(message.chatRoom.id.eq(chatRoom.id)) + .groupBy(message.chatRoom.id) + )).fetchJoin() + .where(isSellerOrWinner(userId)) + .fetch(); + + return sortByLastMessageIdDesc(unsortedDtos); + } + + private List sortByLastMessageIdDesc( + final List unsortedDtos + ) { + return unsortedDtos.stream() + .filter((ChatRoomAndMessageAndImageQueryProjectionDto unsortedDto) -> + Objects.nonNull(unsortedDto.message()) + ).sorted(comparing( + (ChatRoomAndMessageAndImageQueryProjectionDto unsortedDto) -> + unsortedDto.message().getId() + ).reversed() + ).map(ChatRoomAndMessageAndImageQueryProjectionDto::toSortedDto) + .toList(); + } + + private BooleanExpression isSellerOrWinner(final Long userId) { + return (auction.seller.id.eq(userId)) + .or(chatRoom.buyer.id.eq(userId)); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomRepository.java index a45fbaaff..c789bff0b 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomRepository.java @@ -1,15 +1,8 @@ package com.ddang.ddang.chat.infrastructure.persistence; -import com.ddang.ddang.chat.domain.ChatRoom; - -import java.util.List; import java.util.Optional; public interface QuerydslChatRoomRepository { - List findAllByUserId(final Long userId); - - Optional findChatRoomById(final Long chatRoomId); - - Optional findByAuctionId(final Long auctionId); + Optional findChatRoomIdByAuctionId(final Long auctionId); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomRepositoryImpl.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomRepositoryImpl.java index 78a223a52..b3b8908d6 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomRepositoryImpl.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomRepositoryImpl.java @@ -1,17 +1,11 @@ package com.ddang.ddang.chat.infrastructure.persistence; -import com.ddang.ddang.chat.domain.ChatRoom; -import com.querydsl.core.types.dsl.BooleanExpression; -import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; -import java.util.List; import java.util.Optional; -import static com.ddang.ddang.auction.domain.QAuction.auction; -import static com.ddang.ddang.category.domain.QCategory.category; import static com.ddang.ddang.chat.domain.QChatRoom.chatRoom; @Repository @@ -21,42 +15,12 @@ public class QuerydslChatRoomRepositoryImpl implements QuerydslChatRoomRepositor private final JPAQueryFactory queryFactory; @Override - public List findAllByUserId(final Long userId) { - return findChatRoomJPAQuery().where(isSellerOrWinner(userId)) - .orderBy(chatRoom.id.desc()) - .fetch(); - } - - private JPAQuery findChatRoomJPAQuery() { - return queryFactory.selectFrom(chatRoom) - .leftJoin(chatRoom.buyer).fetchJoin() - .leftJoin(chatRoom.auction, auction).fetchJoin() - .leftJoin(auction.subCategory, category).fetchJoin() - .leftJoin(category.mainCategory).fetchJoin() - .leftJoin(auction.seller).fetchJoin() - .leftJoin(auction.auctionImages).fetchJoin(); - } - - private BooleanExpression isSellerOrWinner(final Long userId) { - return (auction.seller.id.eq(userId)) - .or(chatRoom.buyer.id.eq(userId)); - } - - @Override - public Optional findChatRoomById(final Long chatRoomId) { - final ChatRoom findChatRoom = findChatRoomJPAQuery() - .where(chatRoom.id.eq(chatRoomId)) - .fetchOne(); - - return Optional.ofNullable(findChatRoom); - } - - @Override - public Optional findByAuctionId(final Long auctionId) { - final ChatRoom findChatRoom = findChatRoomJPAQuery() - .where(auction.id.eq(auctionId)) - .fetchOne(); + public Optional findChatRoomIdByAuctionId(final Long auctionId) { + final Long chatRoomId = queryFactory.select(chatRoom.id) + .from(chatRoom) + .where(chatRoom.auction.id.eq(auctionId)) + .fetchFirst(); - return Optional.ofNullable(findChatRoom); + return Optional.ofNullable(chatRoomId); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslMessageRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslMessageRepository.java index 806e75f39..ad979d450 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslMessageRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslMessageRepository.java @@ -3,11 +3,11 @@ import com.ddang.ddang.chat.domain.Message; import java.util.List; -import java.util.Optional; public interface QuerydslMessageRepository { - Optional findLastMessageByChatRoomId(final Long chatRoomId); - - List findMessagesAllByLastMessageId(final Long userId, final Long chatRoomId, final Long lastMessageId); + List findMessagesAllByLastMessageId( + final Long messageReaderId, + final Long chatRoomId, + final Long lastMessageId); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslMessageRepositoryImpl.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslMessageRepositoryImpl.java index fb695478d..65b9cfd90 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslMessageRepositoryImpl.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslMessageRepositoryImpl.java @@ -6,15 +6,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; -import java.util.Optional; - -import static com.ddang.ddang.chat.domain.QMessage.message; import java.util.List; -import static com.ddang.ddang.auction.domain.QAuction.auction; -import static com.ddang.ddang.chat.domain.QChatRoom.chatRoom; import static com.ddang.ddang.chat.domain.QMessage.message; -import static com.ddang.ddang.user.domain.QUser.user; @Repository @RequiredArgsConstructor @@ -22,28 +16,16 @@ public class QuerydslMessageRepositoryImpl implements QuerydslMessageRepository private final JPAQueryFactory queryFactory; - @Override - public Optional findLastMessageByChatRoomId(final Long chatRoomId) { - final Message findLastMessage = queryFactory.selectFrom(message) - .leftJoin(message.chatRoom).fetchJoin() - .where(message.chatRoom.id.eq(chatRoomId)) - .orderBy(message.id.desc()) - .fetchFirst(); - - return Optional.ofNullable(findLastMessage); - } - public List findMessagesAllByLastMessageId( - final Long userId, + final Long messageReaderId, final Long chatRoomId, final Long lastMessageId ) { return queryFactory .selectFrom(message) - .leftJoin(message.chatRoom, chatRoom).fetchJoin() .where( - message.writer.id.eq(userId) - .or(message.receiver.id.eq(userId)), + message.writer.id.eq(messageReaderId) + .or(message.receiver.id.eq(messageReaderId)), message.chatRoom.id.eq(chatRoomId), isGreaterThanLastId(lastMessageId) ) diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/dto/ChatRoomAndImageDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/dto/ChatRoomAndImageDto.java new file mode 100644 index 000000000..1b690588c --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/dto/ChatRoomAndImageDto.java @@ -0,0 +1,7 @@ +package com.ddang.ddang.chat.infrastructure.persistence.dto; + +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.image.domain.AuctionImage; + +public record ChatRoomAndImageDto(ChatRoom chatRoom, AuctionImage thumbnailImage) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/dto/ChatRoomAndImageQueryProjectionDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/dto/ChatRoomAndImageQueryProjectionDto.java new file mode 100644 index 000000000..4e52de7c8 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/dto/ChatRoomAndImageQueryProjectionDto.java @@ -0,0 +1,17 @@ +package com.ddang.ddang.chat.infrastructure.persistence.dto; + +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.image.domain.AuctionImage; +import com.querydsl.core.annotations.QueryProjection; + +public record ChatRoomAndImageQueryProjectionDto(ChatRoom chatRoom, AuctionImage auctionImage) { + + // TODO: 2023/09/19 네이밍 컨벤션 회의 후 리팩토링 예정 + @QueryProjection + public ChatRoomAndImageQueryProjectionDto { + } + + public ChatRoomAndImageDto toDto() { + return new ChatRoomAndImageDto(this.chatRoom, this.auctionImage); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/dto/ChatRoomAndMessageAndImageDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/dto/ChatRoomAndMessageAndImageDto.java new file mode 100644 index 000000000..bafad0a34 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/dto/ChatRoomAndMessageAndImageDto.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.chat.infrastructure.persistence.dto; + +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.image.domain.AuctionImage; + +public record ChatRoomAndMessageAndImageDto(ChatRoom chatRoom, Message message, AuctionImage thumbnailImage) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/dto/ChatRoomAndMessageAndImageQueryProjectionDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/dto/ChatRoomAndMessageAndImageQueryProjectionDto.java new file mode 100644 index 000000000..a20e2b827 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/dto/ChatRoomAndMessageAndImageQueryProjectionDto.java @@ -0,0 +1,22 @@ +package com.ddang.ddang.chat.infrastructure.persistence.dto; + +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.image.domain.AuctionImage; +import com.querydsl.core.annotations.QueryProjection; + +public record ChatRoomAndMessageAndImageQueryProjectionDto(ChatRoom chatRoom, Message message, AuctionImage auctionImage) { + + // TODO: 2023/09/19 네이밍 컨벤션 회의 후 리팩토링 예정 + @QueryProjection + public ChatRoomAndMessageAndImageQueryProjectionDto { + } + + public ChatRoomAndMessageAndImageDto toSortedDto() { + return new ChatRoomAndMessageAndImageDto( + this.chatRoom, + this.message, + this.auctionImage + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/ChatRoomController.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/ChatRoomController.java index 933157e67..8067019e9 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/ChatRoomController.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/ChatRoomController.java @@ -17,6 +17,7 @@ import com.ddang.ddang.chat.presentation.dto.response.ReadChatRoomResponse; import com.ddang.ddang.chat.presentation.dto.response.ReadChatRoomWithLastMessageResponse; import com.ddang.ddang.chat.presentation.dto.response.ReadMessageResponse; +import com.ddang.ddang.image.presentation.util.ImageRelativeUrl; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -27,7 +28,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import java.net.URI; import java.util.List; @@ -37,13 +37,11 @@ @RequiredArgsConstructor public class ChatRoomController { - private static final String AUCTIONS_IMAGE_BASE_URL = "/auctions/images/"; - private final ChatRoomService chatRoomService; private final MessageService messageService; @PostMapping - public ResponseEntity create( + public ResponseEntity createChatRoom( @AuthenticateUser final AuthenticationUserInfo userInfo, @RequestBody @Valid final CreateChatRoomRequest chatRoomRequest ) { @@ -63,26 +61,19 @@ public ResponseEntity> readAllParticip final List responses = readParticipatingChatRoomDtos.stream() - .map(dto -> ReadChatRoomWithLastMessageResponse.of(dto, calculateBaseImageUrl())) + .map(ReadChatRoomWithLastMessageResponse::from) .toList(); return ResponseEntity.ok(responses); } - private String calculateBaseImageUrl() { - return ServletUriComponentsBuilder.fromCurrentContextPath() - .build() - .toUriString() - .concat(AUCTIONS_IMAGE_BASE_URL); - } - @GetMapping("/{chatRoomId}") public ResponseEntity readChatRoomById( - @PathVariable final Long chatRoomId, - @AuthenticateUser final AuthenticationUserInfo userInfo + @AuthenticateUser final AuthenticationUserInfo userInfo, + @PathVariable final Long chatRoomId ) { final ReadParticipatingChatRoomDto chatRoomDto = chatRoomService.readByChatRoomId(chatRoomId, userInfo.userId()); - final ReadChatRoomResponse response = ReadChatRoomResponse.of(chatRoomDto, calculateBaseImageUrl()); + final ReadChatRoomResponse response = ReadChatRoomResponse.from(chatRoomDto); return ResponseEntity.ok(response); } @@ -93,7 +84,10 @@ public ResponseEntity createMessage( @PathVariable final Long chatRoomId, @RequestBody @Valid final CreateMessageRequest request ) { - final Long messageId = messageService.create(CreateMessageDto.of(userInfo.userId(), chatRoomId, request)); + + final Long messageId = messageService.create( + CreateMessageDto.of(userInfo.userId(), chatRoomId, request), ImageRelativeUrl.USER.calculateAbsoluteUrl() + ); final CreateMessageResponse response = new CreateMessageResponse(messageId); return ResponseEntity.created(URI.create("/chattings/" + chatRoomId)) @@ -106,7 +100,11 @@ public ResponseEntity> readAllByLastMessageId( @PathVariable final Long chatRoomId, @RequestParam(required = false) final Long lastMessageId ) { - final ReadMessageRequest readMessageRequest = new ReadMessageRequest(userInfo.userId(), chatRoomId, lastMessageId); + final ReadMessageRequest readMessageRequest = new ReadMessageRequest( + userInfo.userId(), + chatRoomId, + lastMessageId + ); final List readMessageDtos = messageService.readAllByLastMessageId(readMessageRequest); final List responses = readMessageDtos.stream() .map(readMessageDto -> ReadMessageResponse.of( @@ -121,8 +119,7 @@ public ResponseEntity> readAllByLastMessageId( } private boolean isMessageOwner(final ReadMessageDto readMessageDto, final AuthenticationUserInfo userInfo) { - return readMessageDto.writerDto() - .id() + return readMessageDto.writerId() .equals(userInfo.userId()); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/request/ReadMessageRequest.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/request/ReadMessageRequest.java index 0682b20fb..1114e6722 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/request/ReadMessageRequest.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/request/ReadMessageRequest.java @@ -1,4 +1,4 @@ package com.ddang.ddang.chat.presentation.dto.request; -public record ReadMessageRequest(Long userId, Long chatRoomId, Long lastMessageId) { +public record ReadMessageRequest(Long messageReaderId, Long chatRoomId, Long lastMessageId) { } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadAuctionInChatRoomResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadAuctionInChatRoomResponse.java index 5659751e2..be45b0034 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadAuctionInChatRoomResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadAuctionInChatRoomResponse.java @@ -1,13 +1,14 @@ package com.ddang.ddang.chat.presentation.dto.response; import com.ddang.ddang.chat.application.dto.ReadAuctionInChatRoomDto; +import com.ddang.ddang.image.presentation.util.ImageRelativeUrl; +import com.ddang.ddang.image.presentation.util.ImageUrlCalculator; public record ReadAuctionInChatRoomResponse(Long id, String title, String image, int price) { - public static ReadAuctionInChatRoomResponse of(final ReadAuctionInChatRoomDto dto, final String baseUrl) { - final Long thumbNailImageId = dto.auctionImageIds().get(0); - final String imageUrl = baseUrl.concat(String.valueOf(thumbNailImageId)); + public static ReadAuctionInChatRoomResponse from(final ReadAuctionInChatRoomDto dto) { + final String thumbNailImageUrl = ImageUrlCalculator.calculateBy(ImageRelativeUrl.AUCTION, dto.thumbnailImageId()); - return new ReadAuctionInChatRoomResponse(dto.id(), dto.title(), imageUrl, dto.lastBidPrice()); + return new ReadAuctionInChatRoomResponse(dto.id(), dto.title(), thumbNailImageUrl, dto.lastBidPrice()); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatPartnerResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatPartnerResponse.java index f49c685ee..2a2daab62 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatPartnerResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatPartnerResponse.java @@ -1,10 +1,19 @@ package com.ddang.ddang.chat.presentation.dto.response; import com.ddang.ddang.chat.application.dto.ReadUserInChatRoomDto; +import com.ddang.ddang.image.presentation.util.ImageRelativeUrl; +import com.ddang.ddang.image.presentation.util.ImageUrlCalculator; +import com.ddang.ddang.user.presentation.util.NameProcessor; public record ReadChatPartnerResponse(Long id, String name, String profileImage) { public static ReadChatPartnerResponse from(final ReadUserInChatRoomDto dto) { - return new ReadChatPartnerResponse(dto.id(), dto.name(), dto.profileImage()); + final String name = NameProcessor.process(dto.isDeleted(), dto.name()); + + return new ReadChatPartnerResponse( + dto.id(), + name, + ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, dto.profileImageId()) + ); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatRoomResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatRoomResponse.java index f22e43178..f0937a8a2 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatRoomResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatRoomResponse.java @@ -9,9 +9,9 @@ public record ReadChatRoomResponse( boolean isChatAvailable ) { - public static ReadChatRoomResponse of(final ReadParticipatingChatRoomDto chatRoomDto, final String baseUrl) { + public static ReadChatRoomResponse from(final ReadParticipatingChatRoomDto chatRoomDto) { final ReadChatPartnerResponse chatPartner = ReadChatPartnerResponse.from(chatRoomDto.partnerDto()); - final ReadAuctionInChatRoomResponse auction = ReadAuctionInChatRoomResponse.of(chatRoomDto.auctionDto(), baseUrl); + final ReadAuctionInChatRoomResponse auction = ReadAuctionInChatRoomResponse.from(chatRoomDto.auctionDto()); return new ReadChatRoomResponse(chatRoomDto.id(), chatPartner, auction, chatRoomDto.isChatAvailable()); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatRoomWithLastMessageResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatRoomWithLastMessageResponse.java index f4eb736ae..7aa99edc0 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatRoomWithLastMessageResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatRoomWithLastMessageResponse.java @@ -10,11 +10,11 @@ public record ReadChatRoomWithLastMessageResponse( boolean isChatAvailable ) { - public static ReadChatRoomWithLastMessageResponse of(final ReadChatRoomWithLastMessageDto dto, final String baseUrl) { + public static ReadChatRoomWithLastMessageResponse from(final ReadChatRoomWithLastMessageDto dto) { return new ReadChatRoomWithLastMessageResponse( dto.id(), ReadChatPartnerResponse.from(dto.partnerDto()), - ReadAuctionInChatRoomResponse.of(dto.auctionDto(), baseUrl), + ReadAuctionInChatRoomResponse.from(dto.auctionDto()), ReadLastMessageResponse.from(dto.lastMessageDto()), dto.isChatAvailable() ); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/common/helper/QuerydslSliceHelper.java b/backend/ddang/src/main/java/com/ddang/ddang/common/helper/QuerydslSliceHelper.java index b23af747d..cfb21df88 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/common/helper/QuerydslSliceHelper.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/common/helper/QuerydslSliceHelper.java @@ -1,32 +1,31 @@ package com.ddang.ddang.common.helper; import java.util.List; -import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl; -public class QuerydslSliceHelper { +public final class QuerydslSliceHelper { private QuerydslSliceHelper() { } - public static Slice toSlice(List contents, int size) { + public static Slice toSlice(final List contents, final Pageable pageable) { + final int size = pageable.getPageSize(); final boolean hasNext = isContentSizeGreaterThanPageSize(contents, size); - final Pageable pageable = PageRequest.ofSize(size); if (hasNext) { - return new SliceImpl<>(subListLastContent(contents, size), pageable, hasNext); + return new SliceImpl<>(getSubListAfterLastContent(contents, size), pageable, hasNext); } return new SliceImpl<>(contents, pageable, hasNext); } - private static boolean isContentSizeGreaterThanPageSize(List content, int size) { - return content.size() > size; + private static boolean isContentSizeGreaterThanPageSize(final List contents, final int size) { + return contents.size() > size; } - private static List subListLastContent(List content, int size) { + private static List getSubListAfterLastContent(final List content, final int size) { return content.subList(0, size); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/configuration/CacheConfiguration.java b/backend/ddang/src/main/java/com/ddang/ddang/configuration/CacheConfiguration.java new file mode 100644 index 000000000..94700483b --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/configuration/CacheConfiguration.java @@ -0,0 +1,9 @@ +package com.ddang.ddang.configuration; + +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableCaching +public class CacheConfiguration { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/configuration/fcm/LocalFcmConfiguration.java b/backend/ddang/src/main/java/com/ddang/ddang/configuration/fcm/LocalFcmConfiguration.java new file mode 100644 index 000000000..d2bf7a58a --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/configuration/fcm/LocalFcmConfiguration.java @@ -0,0 +1,40 @@ +package com.ddang.ddang.configuration.fcm; + +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import java.util.List; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +@Configuration +@Profile("local") +public class LocalFcmConfiguration { + + @Bean + public FirebaseMessaging firebaseMessaging() { + final FirebaseApp firebaseApps = findFirebaseApps(); + + return FirebaseMessaging.getInstance(firebaseApps); + } + + private FirebaseApp findFirebaseApps() { + final List apps = FirebaseApp.getApps(); + + if (!apps.isEmpty()) { + for (final FirebaseApp app : apps) { + if (FirebaseApp.DEFAULT_APP_NAME.equals(app.getName())) { + return app; + } + } + } + + final FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + + return FirebaseApp.initializeApp(options); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/configuration/fcm/MockGoogleCredentials.java b/backend/ddang/src/main/java/com/ddang/ddang/configuration/fcm/MockGoogleCredentials.java new file mode 100644 index 000000000..e8e53ef6f --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/configuration/fcm/MockGoogleCredentials.java @@ -0,0 +1,29 @@ +package com.ddang.ddang.configuration.fcm; + +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; +import lombok.EqualsAndHashCode; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +@EqualsAndHashCode(callSuper = false) +public class MockGoogleCredentials extends GoogleCredentials { + + private final String tokenValue; + private final long expiryTime; + + public MockGoogleCredentials(String tokenValue) { + this(tokenValue, System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)); + } + + public MockGoogleCredentials(String tokenValue, long expiryTime) { + this.tokenValue = tokenValue; + this.expiryTime = expiryTime; + } + + @Override + public AccessToken refreshAccessToken() { + return new AccessToken(tokenValue, new Date(expiryTime)); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/configuration/fcm/ProdFcmConfiguration.java b/backend/ddang/src/main/java/com/ddang/ddang/configuration/fcm/ProdFcmConfiguration.java new file mode 100644 index 000000000..ac0848a4b --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/configuration/fcm/ProdFcmConfiguration.java @@ -0,0 +1,54 @@ +package com.ddang.ddang.configuration.fcm; + +import com.ddang.ddang.configuration.fcm.exception.FcmNotFoundException; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import org.springframework.context.annotation.Profile; + +@Configuration +@Profile("!test && !local") +public class ProdFcmConfiguration { + + @Value("${fcm.key.path}") + private String FCM_PRIVATE_KEY_PATH; + + @Bean + FirebaseMessaging firebaseMessaging() throws IOException { + final FileInputStream refreshToken = new FileInputStream(FCM_PRIVATE_KEY_PATH); + final List firebaseApps = FirebaseApp.getApps(); + + if (firebaseApps.isEmpty()) { + return makeNewInstance(refreshToken); + } + + refreshToken.close(); + return findExistingInstance(firebaseApps); + } + + private FirebaseMessaging makeNewInstance(final InputStream refreshToken) throws IOException { + final FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(refreshToken)) + .build(); + refreshToken.close(); + + return FirebaseMessaging.getInstance(FirebaseApp.initializeApp(options)); + } + + private FirebaseMessaging findExistingInstance(final List firebaseApps) { + return firebaseApps.stream() + .filter(app -> app.getName().equals(FirebaseApp.DEFAULT_APP_NAME)) + .findAny() + .map(FirebaseMessaging::getInstance) + .orElseThrow(FcmNotFoundException::new); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/configuration/fcm/exception/FcmNotFoundException.java b/backend/ddang/src/main/java/com/ddang/ddang/configuration/fcm/exception/FcmNotFoundException.java new file mode 100644 index 000000000..b2bb8be99 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/configuration/fcm/exception/FcmNotFoundException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.configuration.fcm.exception; + +public class FcmNotFoundException extends RuntimeException { + + public FcmNotFoundException() { + super("현재 Firebase App에 접근할 수 없습니다."); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/configuration/initialization/InitializationUserConfiguration.java b/backend/ddang/src/main/java/com/ddang/ddang/configuration/initialization/InitializationUserConfiguration.java index 2c97d719c..eca2b6d86 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/configuration/initialization/InitializationUserConfiguration.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/configuration/initialization/InitializationUserConfiguration.java @@ -1,5 +1,7 @@ package com.ddang.ddang.configuration.initialization; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; import com.ddang.ddang.user.domain.User; import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; import lombok.RequiredArgsConstructor; @@ -14,7 +16,6 @@ @RequiredArgsConstructor public class InitializationUserConfiguration implements ApplicationRunner { - private static final String IMAGE_URL = "https://img1.daumcdn.net/thumb/R1280x0/?fname=http://t1.daumcdn.net/brunch/service/user/7r5X/image/9djEiPBPMLu_IvCYyvRPwmZkM1g.jpg"; private final JpaUserRepository userRepository; @Override @@ -22,22 +23,22 @@ public class InitializationUserConfiguration implements ApplicationRunner { public void run(final ApplicationArguments args) { final User seller1 = User.builder() .name("판매자1") - .profileImage(IMAGE_URL) - .reliability(4.7d) + .profileImage(new ProfileImage("upload.png", "updateImage.png")) + .reliability(new Reliability(4.7d)) .oauthId("12345") .build(); final User buyer1 = User.builder() .name("구매자1") - .profileImage(IMAGE_URL) - .reliability(3.0d) + .profileImage(new ProfileImage("upload.png", "updateImage.png")) + .reliability(new Reliability(3.0d)) .oauthId("12346") .build(); final User buyer2 = User.builder() .name("구매자2") - .profileImage(IMAGE_URL) - .reliability(0.8d) + .profileImage(new ProfileImage("upload.png", "updateImage.png")) + .reliability(new Reliability(0.8d)) .oauthId("12347") .build(); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/LogTracer.java b/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/LogTracer.java new file mode 100644 index 000000000..15fe951d4 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/LogTracer.java @@ -0,0 +1,124 @@ +package com.ddang.ddang.configuration.log; + +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class LogTracer { + + private static final String START_PREFIX = "---> "; + private static final String COMPLETE_PREFIX = "<--- "; + private static final String EXCEPTION_PREFIX = "<-X- "; + private static final String FIRST_DEPTH_SPACE = "|"; + private static final String OTHER_DEPTH_SPACE = "| "; + + private final ThreadLocal traceIdHolder = new ThreadLocal<>(); + + public TraceStatus begin(final String className, final String methodName) { + syncTraceDepth(); + + final TraceDepth traceId = traceIdHolder.get(); + final Long startTime = System.currentTimeMillis(); + final String methodSignature = formattedMethodSignature(className, methodName); + + MDC.put("level", String.valueOf(traceId.getLevel())); + MDC.put("resultTime", "NONE"); + MDC.put("class.method", methodSignature); + + log.info("{}", formattedDepth(addSpace(START_PREFIX, traceId.getLevel()), methodSignature)); + + return new TraceStatus(traceId, startTime, methodName); + } + + private void syncTraceDepth() { + final TraceDepth traceId = traceIdHolder.get(); + + traceIdHolder.set(findNextTraceDepth(traceId)); + } + + private TraceDepth findNextTraceDepth(final TraceDepth traceId) { + if (traceId == null) { + return new TraceDepth(); + } + + return traceId.createNextId(); + } + + private String formattedMethodSignature(final String className, final String methodName) { + return className + "." + methodName + "()"; + } + + private String formattedDepth(final String prefix, final String methodSignature) { + return prefix + methodSignature; + } + + private String addSpace(final String prefix, final int level) { + final StringBuilder spaceBuilder = new StringBuilder(); + + for (int depth = 0; depth < level; depth++) { + spaceBuilder.append(findNextSpace(prefix, depth, level)); + } + + return spaceBuilder.toString(); + } + + private String findNextSpace(final String prefix, final int depth, final int level) { + if (depth == level - 1) { + return FIRST_DEPTH_SPACE + prefix; + } + + return OTHER_DEPTH_SPACE; + } + + public void end(final TraceStatus status, final String className, final String methodName) { + complete(status, className, methodName, null); + } + + private void complete( + final TraceStatus status, + final String className, + final String methodName, + final Throwable ex + ) { + final Long stopTime = System.currentTimeMillis(); + final long resultTime = stopTime - status.getStartTime(); + final TraceDepth traceId = status.getTraceDepth(); + final String methodSignature = formattedMethodSignature(className, methodName); + + MDC.put("level", String.valueOf(traceId.getLevel())); + MDC.put("resultTime", resultTime + "ms"); + MDC.put("class.method", methodSignature); + + if (ex == null) { + log.info("{}", formattedDepth(addSpace(COMPLETE_PREFIX, traceId.getLevel()), methodSignature)); + } else { + log.info("{} : {}", formattedDepth( + addSpace(EXCEPTION_PREFIX, traceId.getLevel()), methodSignature), + ex.getClass().getSimpleName() + ); + } + + releaseTraceDepth(); + } + + private void releaseTraceDepth() { + final TraceDepth traceId = traceIdHolder.get(); + + if (traceId.isFirstLevel()) { + traceIdHolder.remove(); + } else { + traceIdHolder.set(traceId.createPreviousId()); + } + } + + public void exception( + final TraceStatus status, + final String className, + final String methodName, + final Throwable ex + ) { + complete(status, className, methodName, ex); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/LogTracerAop.java b/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/LogTracerAop.java new file mode 100644 index 000000000..f58f02a07 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/LogTracerAop.java @@ -0,0 +1,67 @@ +package com.ddang.ddang.configuration.log; + +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; + +@Aspect +@Component +@RequiredArgsConstructor +public class LogTracerAop { + + private static final String PROXY_CLASS_PREFIX = "Proxy"; + + private final LogTracer logTracer; + + @Pointcut("@within(org.springframework.stereotype.Service)") + private void serviceAnnotatedClass() { + } + + @Pointcut("execution(* com.ddang.ddang..*Repository+.*(..))") + private void repositoryClass() { + } + + @Around("serviceAnnotatedClass() || repositoryClass()") + public Object doLog(final ProceedingJoinPoint joinPoint) throws Throwable { + if (isNotRequestScope()) { + return joinPoint.proceed(); + } + + final String className = findClassSimpleName(joinPoint); + final String methodName = findMethodName(joinPoint); + final TraceStatus status = logTracer.begin(className, methodName); + + try { + final Object result = joinPoint.proceed(); + + logTracer.end(status, className, methodName); + return result; + } catch (final Throwable ex) { + logTracer.exception(status, className, methodName, ex); + + throw ex; + } + } + + private boolean isNotRequestScope() { + return RequestContextHolder.getRequestAttributes() == null; + } + + private String findClassSimpleName(final ProceedingJoinPoint joinPoint) { + final Class clazz = joinPoint.getTarget().getClass(); + final String className = clazz.getSimpleName(); + + if (className.contains(PROXY_CLASS_PREFIX)) { + return clazz.getInterfaces()[0].getSimpleName(); + } + return className; + } + + private String findMethodName(final ProceedingJoinPoint joinPoint) { + return joinPoint.getSignature().getName(); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/MdcFilter.java b/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/MdcFilter.java new file mode 100644 index 000000000..4b0ed3af9 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/MdcFilter.java @@ -0,0 +1,65 @@ +package com.ddang.ddang.configuration.log; + +import com.ddang.ddang.authentication.domain.TokenDecoder; +import com.ddang.ddang.authentication.domain.TokenType; +import com.ddang.ddang.authentication.domain.exception.InvalidTokenException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.slf4j.MDC; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@Component +@Profile("!test") +@RequiredArgsConstructor +public class MdcFilter extends OncePerRequestFilter { + + private static final String REQUEST_ID_HEADER_KEY = "X-Request-Id"; + private static final String UNAUTHORIZED_USER_ID = "Unauthorized"; + + private final TokenDecoder tokenDecoder; + + @Override + protected void doFilterInternal( + final HttpServletRequest request, + final HttpServletResponse response, + final FilterChain filterChain + ) throws ServletException, IOException { + MDC.put("requestId", findRequestId(request)); + MDC.put("requestUri", request.getRequestURI()); + MDC.put("userId", String.valueOf(findUserId(request))); + + filterChain.doFilter(request, response); + + MDC.clear(); + } + + private String findRequestId(final HttpServletRequest request) { + final String requestId = request.getHeader(REQUEST_ID_HEADER_KEY); + + if (requestId == null || requestId.isEmpty() || requestId.isBlank()) { + return UUID.randomUUID().toString(); + } + + return requestId; + } + + private String findUserId(final HttpServletRequest request) { + final String accessToken = request.getHeader(HttpHeaders.AUTHORIZATION); + + try { + return tokenDecoder.decode(TokenType.ACCESS, accessToken) + .map(privateClaims -> String.valueOf(privateClaims.userId())) + .orElse(UNAUTHORIZED_USER_ID); + } catch (final InvalidTokenException ex) { + return UNAUTHORIZED_USER_ID; + } + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/RequestResponseLogAop.java b/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/RequestResponseLogAop.java new file mode 100644 index 000000000..384033811 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/RequestResponseLogAop.java @@ -0,0 +1,58 @@ +package com.ddang.ddang.configuration.log; + +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; + +@Aspect +@Component +@RequiredArgsConstructor +public class RequestResponseLogAop { + + private final RequestResponseLogProcessor logProcessor; + + @Pointcut("@within(org.springframework.web.bind.annotation.RestController)") + private void restControllerAnnotatedClass() { + } + + @Around("restControllerAnnotatedClass()") + public Object doLog(final ProceedingJoinPoint joinPoint) throws Throwable { + if (isNotRequestScope()) { + return joinPoint.proceed(); + } + + final String className = findClassSimpleName(joinPoint); + final String methodName = findMethodName(joinPoint); + + final TraceStatus status = logProcessor.begin(className, methodName, joinPoint.getArgs()); + + try { + final Object result = joinPoint.proceed(); + + logProcessor.end(status, className, methodName, result); + return result; + } catch (final Throwable ex) { + logProcessor.exception(status, className, methodName, ex); + + throw ex; + } + } + + private boolean isNotRequestScope() { + return RequestContextHolder.getRequestAttributes() == null; + } + + private String findClassSimpleName(final ProceedingJoinPoint joinPoint) { + final Class clazz = joinPoint.getTarget().getClass(); + + return clazz.getSimpleName(); + } + + private String findMethodName(final ProceedingJoinPoint joinPoint) { + return joinPoint.getSignature().getName(); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/RequestResponseLogProcessor.java b/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/RequestResponseLogProcessor.java new file mode 100644 index 000000000..700f5cdab --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/RequestResponseLogProcessor.java @@ -0,0 +1,93 @@ +package com.ddang.ddang.configuration.log; + +import lombok.extern.slf4j.Slf4j; +import net.logstash.logback.marker.Markers; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class RequestResponseLogProcessor { + + private final ThreadLocal traceIdHolder = new ThreadLocal<>(); + + public TraceStatus begin(final String className, final String methodName, final Object[] requestArgs) { + syncTraceDepth(); + + final TraceDepth traceId = traceIdHolder.get(); + final Long startTime = System.currentTimeMillis(); + + MDC.put("level", String.valueOf(traceId.getLevel())); + MDC.put("resultTime", "NONE"); + MDC.put("class.method", formattedMethodSignature(className, methodName)); + + log.info("{}", Markers.appendArray("arguments", requestArgs)); + + return new TraceStatus(traceId, startTime, methodName); + } + + private void syncTraceDepth() { + final TraceDepth traceId = traceIdHolder.get(); + + traceIdHolder.set(findNextTraceDepth(traceId)); + } + + private TraceDepth findNextTraceDepth(final TraceDepth traceId) { + if (traceId == null) { + return new TraceDepth(); + } + + return traceId.createNextId(); + } + + private String formattedMethodSignature(final String className, final String methodName) { + return className + "." + methodName + "()"; + } + + public void end(final TraceStatus status, final String className, final String methodName, final Object result) { + complete(status, className, methodName, result, null); + } + + public void exception( + final TraceStatus status, + final String className, + final String methodName, + final Throwable ex + ) { + complete(status, className, methodName, null, ex); + } + + private void complete( + final TraceStatus status, + final String className, + final String methodName, + final Object target, + final Throwable ex + ) { + final Long stopTime = System.currentTimeMillis(); + final long resultTime = stopTime - status.getStartTime(); + final TraceDepth traceId = status.getTraceDepth(); + + MDC.put("level", String.valueOf(traceId.getLevel())); + MDC.put("resultTime", resultTime + "ms"); + MDC.put("class.method", formattedMethodSignature(className, methodName)); + + if (target != null && ex == null) { + log.info("{}", target); + } else { + log.info("{} : {}", ex.getClass().getSimpleName(), ex.getMessage()); + } + + releaseTraceDepth(); + } + + private void releaseTraceDepth() { + final TraceDepth traceId = traceIdHolder.get(); + + if (traceId.isFirstLevel()) { + traceIdHolder.remove(); + } else { + traceIdHolder.set(traceId.createPreviousId()); + } + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/log/SlackAppender.java b/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/SlackAppender.java similarity index 98% rename from backend/ddang/src/main/java/com/ddang/ddang/log/SlackAppender.java rename to backend/ddang/src/main/java/com/ddang/ddang/configuration/log/SlackAppender.java index 7526df401..d7a62f620 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/log/SlackAppender.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/SlackAppender.java @@ -1,4 +1,4 @@ -package com.ddang.ddang.log; +package com.ddang.ddang.configuration.log; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.AppenderBase; @@ -57,9 +57,3 @@ private String createMessage(final ILoggingEvent eventObject) { eventObject.getFormattedMessage()); } } - - - - - - diff --git a/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/TraceDepth.java b/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/TraceDepth.java new file mode 100644 index 000000000..dfaf42b40 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/TraceDepth.java @@ -0,0 +1,30 @@ +package com.ddang.ddang.configuration.log; + +public class TraceDepth { + + private final int level; + + public TraceDepth() { + this.level = 0; + } + + private TraceDepth(final int level) { + this.level = level; + } + + public TraceDepth createNextId() { + return new TraceDepth(level + 1); + } + + public TraceDepth createPreviousId() { + return new TraceDepth(level - 1); + } + + public boolean isFirstLevel() { + return level == 0; + } + + public int getLevel() { + return level; + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/TraceStatus.java b/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/TraceStatus.java new file mode 100644 index 000000000..6bf1aa221 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/configuration/log/TraceStatus.java @@ -0,0 +1,26 @@ +package com.ddang.ddang.configuration.log; + +public class TraceStatus { + + private final TraceDepth traceDepth; + private final Long startTime; + private final String message; + + public TraceStatus(final TraceDepth traceDepth, final Long startTime, final String message) { + this.traceDepth = traceDepth; + this.startTime = startTime; + this.message = message; + } + + public TraceDepth getTraceDepth() { + return traceDepth; + } + + public Long getStartTime() { + return startTime; + } + + public String getMessage() { + return message; + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/configuration/metric/LogicMetricAop.java b/backend/ddang/src/main/java/com/ddang/ddang/configuration/metric/LogicMetricAop.java new file mode 100644 index 000000000..68843fc76 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/configuration/metric/LogicMetricAop.java @@ -0,0 +1,85 @@ +package com.ddang.ddang.configuration.metric; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; + +@Aspect +@Component +@RequiredArgsConstructor +public class LogicMetricAop { + + private final MeterRegistry registry; + + @Pointcut("@within(org.springframework.web.bind.annotation.RestController)") + private void restControllerAnnotatedClass() { + } + + @Pointcut("within(com.ddang.ddang.image.presentation.ImageController)") + private void imageControllerClass() { + } + + @Pointcut("within(com.ddang.ddang.region.presentation.RegionController)") + private void regionControllerClass() { + } + + @Pointcut("within(com.ddang.ddang.category.presentation.CategoryController)") + private void categoryControllerClass() { + } + + @Pointcut("within(com.ddang.ddang.report.presentation.ReportController)") + private void reportControllerClass() { + } + + @Pointcut("within(com.ddang.ddang.device.presentation.DeviceTokenController)") + private void deviceTokenControllerClass() { + } + + @Pointcut("within(com.ddang.ddang.authentication.presentation.AuthenticationController)") + private void authenticationControllerClass() { + } + + @Around("restControllerAnnotatedClass() && !imageControllerClass() && !regionControllerClass() && " + + "!categoryControllerClass() && !reportControllerClass() && !deviceTokenControllerClass() && " + + "!authenticationControllerClass()") + public Object doLog(final ProceedingJoinPoint joinPoint) throws Throwable { + if (isRequestScope()) { + final String className = findClassSimpleName(joinPoint); + final String methodName = findMethodName(joinPoint); + + Counter.builder("logic") + .tag("class", className) + .tag("method", methodName) + .tag("class.method", formattedTag(className, methodName)) + .description("비즈니스 로직 메트릭") + .register(registry) + .increment(); + } + + return joinPoint.proceed(); + } + + private boolean isRequestScope() { + return RequestContextHolder.getRequestAttributes() != null; + } + + private String findClassSimpleName(final ProceedingJoinPoint joinPoint) { + final Class clazz = joinPoint.getTarget().getClass(); + + return clazz.getSimpleName(); + } + + private String findMethodName(final ProceedingJoinPoint joinPoint) { + return joinPoint.getSignature().getName(); + } + + private String formattedTag(final String className, final String methodName) { + return className + "." + methodName; + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/configuration/MonitoringConfiguration.java b/backend/ddang/src/main/java/com/ddang/ddang/configuration/metric/MetricConfiguration.java similarity index 84% rename from backend/ddang/src/main/java/com/ddang/ddang/configuration/MonitoringConfiguration.java rename to backend/ddang/src/main/java/com/ddang/ddang/configuration/metric/MetricConfiguration.java index 4b044bb73..55d37a174 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/configuration/MonitoringConfiguration.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/configuration/metric/MetricConfiguration.java @@ -1,4 +1,4 @@ -package com.ddang.ddang.configuration; +package com.ddang.ddang.configuration.metric; import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository; import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository; @@ -6,7 +6,7 @@ import org.springframework.context.annotation.Configuration; @Configuration -public class MonitoringConfiguration { +public class MetricConfiguration { @Bean public HttpExchangeRepository httpExchangeRepository() { diff --git a/backend/ddang/src/main/java/com/ddang/ddang/device/application/DeviceTokenService.java b/backend/ddang/src/main/java/com/ddang/ddang/device/application/DeviceTokenService.java new file mode 100644 index 000000000..47ca28e74 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/device/application/DeviceTokenService.java @@ -0,0 +1,43 @@ +package com.ddang.ddang.device.application; + +import com.ddang.ddang.device.application.dto.PersistDeviceTokenDto; +import com.ddang.ddang.device.domain.DeviceToken; +import com.ddang.ddang.device.infrastructure.persistence.JpaDeviceTokenRepository; +import com.ddang.ddang.user.application.exception.UserNotFoundException; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class DeviceTokenService { + + private final JpaDeviceTokenRepository deviceTokenRepository; + private final JpaUserRepository userRepository; + + @Transactional + public void persist(final Long userId, final PersistDeviceTokenDto deviceTokenDto) { + final String newDeviceToken = deviceTokenDto.deviceToken(); + if (newDeviceToken == null || newDeviceToken.isBlank()) { + return; + } + + final DeviceToken deviceToken = + deviceTokenRepository.findByUserId(userId) + .orElseGet(() -> createDeviceToken(userId, newDeviceToken)); + if (deviceToken.isDifferentToken(newDeviceToken)) { + deviceToken.updateDeviceToken(newDeviceToken); + } + } + + private DeviceToken createDeviceToken(final Long userId, final String newDeviceToken) { + final User findUser = userRepository.findById(userId) + .orElseThrow(() -> new UserNotFoundException("해당 사용자를 찾을 수 없습니다.")); + final DeviceToken deviceToken = new DeviceToken(findUser, newDeviceToken); + + return deviceTokenRepository.save(deviceToken); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/device/application/dto/PersistDeviceTokenDto.java b/backend/ddang/src/main/java/com/ddang/ddang/device/application/dto/PersistDeviceTokenDto.java new file mode 100644 index 000000000..394525f48 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/device/application/dto/PersistDeviceTokenDto.java @@ -0,0 +1,10 @@ +package com.ddang.ddang.device.application.dto; + +import com.ddang.ddang.device.presentation.dto.request.UpdateDeviceTokenRequest; + +public record PersistDeviceTokenDto(String deviceToken) { + + public static PersistDeviceTokenDto from(final UpdateDeviceTokenRequest updateDeviceTokenRequest) { + return new PersistDeviceTokenDto(updateDeviceTokenRequest.deviceToken()); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/device/application/exception/DeviceTokenNotFoundException.java b/backend/ddang/src/main/java/com/ddang/ddang/device/application/exception/DeviceTokenNotFoundException.java new file mode 100644 index 000000000..79993dc81 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/device/application/exception/DeviceTokenNotFoundException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.device.application.exception; + +public class DeviceTokenNotFoundException extends IllegalArgumentException { + + public DeviceTokenNotFoundException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/device/domain/DeviceToken.java b/backend/ddang/src/main/java/com/ddang/ddang/device/domain/DeviceToken.java new file mode 100644 index 000000000..3a8fd56bb --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/device/domain/DeviceToken.java @@ -0,0 +1,49 @@ +package com.ddang.ddang.device.domain; + +import com.ddang.ddang.user.domain.User; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@EqualsAndHashCode(of = "id") +@ToString(of = {"id", "deviceToken"}) +public class DeviceToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", foreignKey = @ForeignKey(name = "fk_device_token_user")) + private User user; + + @Column(name = "device_token", unique = true, nullable = false) + private String deviceToken; + + public DeviceToken(final User user, final String deviceToken) { + this.user = user; + this.deviceToken = deviceToken; + } + + public boolean isDifferentToken(final String targetDeviceToken) { + return !this.deviceToken.equals(targetDeviceToken); + } + + public void updateDeviceToken(final String newDeviceToken) { + this.deviceToken = newDeviceToken; + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/device/infrastructure/persistence/JpaDeviceTokenRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/device/infrastructure/persistence/JpaDeviceTokenRepository.java new file mode 100644 index 000000000..13fc6f49b --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/device/infrastructure/persistence/JpaDeviceTokenRepository.java @@ -0,0 +1,13 @@ +package com.ddang.ddang.device.infrastructure.persistence; + +import com.ddang.ddang.device.domain.DeviceToken; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface JpaDeviceTokenRepository extends JpaRepository { + + Optional findByUserId(final Long userId); + + void deleteByUserId(Long id); +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/device/presentation/DeviceTokenController.java b/backend/ddang/src/main/java/com/ddang/ddang/device/presentation/DeviceTokenController.java new file mode 100644 index 000000000..df04596d0 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/device/presentation/DeviceTokenController.java @@ -0,0 +1,33 @@ +package com.ddang.ddang.device.presentation; + +import com.ddang.ddang.authentication.configuration.AuthenticateUser; +import com.ddang.ddang.authentication.domain.dto.AuthenticationUserInfo; +import com.ddang.ddang.device.application.DeviceTokenService; +import com.ddang.ddang.device.application.dto.PersistDeviceTokenDto; +import com.ddang.ddang.device.presentation.dto.request.UpdateDeviceTokenRequest; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/device-token") +@RequiredArgsConstructor +public class DeviceTokenController { + + private final DeviceTokenService deviceTokenService; + + @PatchMapping + ResponseEntity update( + @AuthenticateUser final AuthenticationUserInfo userInfo, + @RequestBody @Valid final UpdateDeviceTokenRequest updateDeviceTokenRequest + ) { + deviceTokenService.persist(userInfo.userId(), PersistDeviceTokenDto.from(updateDeviceTokenRequest)); + + return ResponseEntity.ok() + .build(); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/device/presentation/dto/request/UpdateDeviceTokenRequest.java b/backend/ddang/src/main/java/com/ddang/ddang/device/presentation/dto/request/UpdateDeviceTokenRequest.java new file mode 100644 index 000000000..d7300fdca --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/device/presentation/dto/request/UpdateDeviceTokenRequest.java @@ -0,0 +1,6 @@ +package com.ddang.ddang.device.presentation.dto.request; + +import jakarta.validation.constraints.NotEmpty; + +public record UpdateDeviceTokenRequest(@NotEmpty(message = "기기 토큰이 입력되지 않았습니다.") String deviceToken) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java b/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java index 5b67c17e4..9438d22e0 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java @@ -2,8 +2,10 @@ import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; import com.ddang.ddang.auction.application.exception.UserForbiddenException; +import com.ddang.ddang.auction.configuration.exception.InvalidSearchConditionException; import com.ddang.ddang.auction.domain.exception.InvalidPriceValueException; import com.ddang.ddang.auction.domain.exception.WinnerNotFoundException; +import com.ddang.ddang.authentication.application.exception.InvalidWithdrawalException; import com.ddang.ddang.authentication.configuration.exception.UserUnauthorizedException; import com.ddang.ddang.authentication.domain.exception.InvalidTokenException; import com.ddang.ddang.authentication.domain.exception.UnsupportedSocialLoginException; @@ -11,22 +13,32 @@ import com.ddang.ddang.category.application.exception.CategoryNotFoundException; import com.ddang.ddang.chat.application.exception.ChatRoomNotFoundException; import com.ddang.ddang.chat.application.exception.InvalidAuctionToChatException; +import com.ddang.ddang.chat.application.exception.InvalidUserToChat; import com.ddang.ddang.chat.application.exception.MessageNotFoundException; import com.ddang.ddang.chat.application.exception.UnableToChatException; -import com.ddang.ddang.chat.application.exception.UserNotAccessibleException; +import com.ddang.ddang.device.application.exception.DeviceTokenNotFoundException; import com.ddang.ddang.exception.dto.ExceptionResponse; import com.ddang.ddang.image.application.exception.ImageNotFoundException; import com.ddang.ddang.image.infrastructure.local.exception.EmptyImageException; import com.ddang.ddang.image.infrastructure.local.exception.StoreImageFailureException; import com.ddang.ddang.image.infrastructure.local.exception.UnsupportedImageFileExtensionException; +import com.ddang.ddang.notification.application.exception.NotificationFailedException; +import com.ddang.ddang.qna.application.exception.AlreadyAnsweredException; +import com.ddang.ddang.qna.application.exception.AnswerNotFoundException; +import com.ddang.ddang.qna.application.exception.InvalidAnswererException; +import com.ddang.ddang.qna.application.exception.InvalidAuctionToAskQuestionException; +import com.ddang.ddang.qna.application.exception.InvalidQuestionerException; +import com.ddang.ddang.qna.application.exception.QuestionNotFoundException; import com.ddang.ddang.region.application.exception.RegionNotFoundException; import com.ddang.ddang.report.application.exception.AlreadyReportAuctionException; import com.ddang.ddang.report.application.exception.AlreadyReportChatRoomException; -import com.ddang.ddang.report.application.exception.ChatRoomReportNotAccessibleException; +import com.ddang.ddang.report.application.exception.InvalidChatRoomReportException; +import com.ddang.ddang.report.application.exception.InvalidQuestionReportException; import com.ddang.ddang.report.application.exception.InvalidReportAuctionException; import com.ddang.ddang.report.application.exception.InvalidReporterToAuctionException; +import com.ddang.ddang.review.application.exception.AlreadyReviewException; +import com.ddang.ddang.review.application.exception.ReviewNotFoundException; import com.ddang.ddang.user.application.exception.UserNotFoundException; -import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; @@ -38,12 +50,11 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import java.net.MalformedURLException; -import java.util.stream.Collectors; @RestControllerAdvice public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { - private static final String EXCEPTION_FORMAT = "%s : "; + private static final String LOG_MESSAGE_FORMAT = "%s : %s"; @Override protected ResponseEntity handleExceptionInternal( @@ -53,15 +64,22 @@ protected ResponseEntity handleExceptionInternal( final HttpStatusCode statusCode, final WebRequest request ) { - logger.error(String.format(EXCEPTION_FORMAT, ex.getClass() - .getSimpleName()), ex); + logger.error(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage()), ex); return super.handleExceptionInternal(ex, body, headers, statusCode, request); } + @ExceptionHandler(MalformedURLException.class) + public ResponseEntity handleMalformedURLException(final MalformedURLException ex) { + logger.error(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage()), ex); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ExceptionResponse("이미지 조회에 실패했습니다.")); + } + @ExceptionHandler(CategoryNotFoundException.class) public ResponseEntity handleCategoryNotFoundException(final CategoryNotFoundException ex) { - logger.warn(String.format(EXCEPTION_FORMAT, CategoryNotFoundException.class), ex); + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(new ExceptionResponse(ex.getMessage())); @@ -69,7 +87,7 @@ public ResponseEntity handleCategoryNotFoundException(final C @ExceptionHandler(RegionNotFoundException.class) public ResponseEntity handleRegionNotFoundException(final RegionNotFoundException ex) { - logger.warn(String.format(EXCEPTION_FORMAT, RegionNotFoundException.class), ex); + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(new ExceptionResponse(ex.getMessage())); @@ -77,7 +95,7 @@ public ResponseEntity handleRegionNotFoundException(final Reg @ExceptionHandler(ChatRoomNotFoundException.class) public ResponseEntity handleChatRoomNotFoundException(final ChatRoomNotFoundException ex) { - logger.warn(String.format(EXCEPTION_FORMAT, ChatRoomNotFoundException.class), ex); + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(new ExceptionResponse(ex.getMessage())); @@ -85,7 +103,7 @@ public ResponseEntity handleChatRoomNotFoundException(final C @ExceptionHandler(MessageNotFoundException.class) public ResponseEntity handleMessageNotFoundException(final MessageNotFoundException ex) { - logger.warn(String.format(EXCEPTION_FORMAT, MessageNotFoundException.class), ex); + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(new ExceptionResponse(ex.getMessage())); @@ -93,7 +111,7 @@ public ResponseEntity handleMessageNotFoundException(final Me @ExceptionHandler(UnableToChatException.class) public ResponseEntity handleUnableToChatException(final UnableToChatException ex) { - logger.warn(String.format(EXCEPTION_FORMAT, MessageNotFoundException.class), ex); + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(new ExceptionResponse(ex.getMessage())); @@ -101,7 +119,7 @@ public ResponseEntity handleUnableToChatException(final Unabl @ExceptionHandler(UserNotFoundException.class) public ResponseEntity handleUserNotFoundException(final UserNotFoundException ex) { - logger.warn(String.format(EXCEPTION_FORMAT, UserNotFoundException.class), ex); + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(new ExceptionResponse(ex.getMessage())); @@ -109,7 +127,7 @@ public ResponseEntity handleUserNotFoundException(final UserN @ExceptionHandler(AuctionNotFoundException.class) public ResponseEntity handleAuctionNotFoundException(final AuctionNotFoundException ex) { - logger.warn(String.format(EXCEPTION_FORMAT, AuctionNotFoundException.class), ex); + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(new ExceptionResponse(ex.getMessage())); @@ -117,7 +135,7 @@ public ResponseEntity handleAuctionNotFoundException(final Au @ExceptionHandler(InvalidBidException.class) public ResponseEntity handleInvalidBidException(final InvalidBidException ex) { - logger.warn(String.format(EXCEPTION_FORMAT, InvalidBidException.class), ex); + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(new ExceptionResponse(ex.getMessage())); @@ -127,17 +145,27 @@ public ResponseEntity handleInvalidBidException(final Invalid public ResponseEntity handleUserNotAuthorizationException( final UserForbiddenException ex ) { - logger.warn(String.format(EXCEPTION_FORMAT, UserForbiddenException.class), ex); + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(new ExceptionResponse(ex.getMessage())); } + @ExceptionHandler(InvalidWithdrawalException.class) + public ResponseEntity handleInaccessibleWithdrawalException( + final InvalidWithdrawalException ex + ) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); + + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(new ExceptionResponse(ex.getMessage())); + } + @ExceptionHandler(InvalidPriceValueException.class) public ResponseEntity handleInvalidPriceValueException( final InvalidPriceValueException ex ) { - logger.warn(String.format(EXCEPTION_FORMAT, InvalidPriceValueException.class), ex); + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(new ExceptionResponse(ex.getMessage())); @@ -145,7 +173,7 @@ public ResponseEntity handleInvalidPriceValueException( @ExceptionHandler(EmptyImageException.class) public ResponseEntity handleEmptyImageException(final EmptyImageException ex) { - logger.warn(String.format(EXCEPTION_FORMAT, EmptyImageException.class), ex); + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(new ExceptionResponse(ex.getMessage())); @@ -155,7 +183,7 @@ public ResponseEntity handleEmptyImageException(final EmptyIm public ResponseEntity handleStoreImageFailureException( final StoreImageFailureException ex ) { - logger.error(String.format(EXCEPTION_FORMAT, StoreImageFailureException.class), ex); + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(new ExceptionResponse(ex.getMessage())); @@ -165,7 +193,7 @@ public ResponseEntity handleStoreImageFailureException( public ResponseEntity handleUnsupportedImageFileExtensionException( final UnsupportedImageFileExtensionException ex ) { - logger.warn(String.format(EXCEPTION_FORMAT, UnsupportedImageFileExtensionException.class), ex); + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(new ExceptionResponse(ex.getMessage())); @@ -173,25 +201,17 @@ public ResponseEntity handleUnsupportedImageFileExtensionExce @ExceptionHandler(ImageNotFoundException.class) public ResponseEntity handleImageNotFoundException(final ImageNotFoundException ex) { - logger.warn(String.format(EXCEPTION_FORMAT, ImageNotFoundException.class), ex); + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(new ExceptionResponse(ex.getMessage())); } - @ExceptionHandler(MalformedURLException.class) - public ResponseEntity handleMalformedURLException(final MalformedURLException ex) { - logger.warn(String.format(EXCEPTION_FORMAT, MalformedURLException.class), ex); - - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(new ExceptionResponse("이미지 조회에 실패했습니다.")); - } - - @ExceptionHandler(UserNotAccessibleException.class) + @ExceptionHandler(InvalidUserToChat.class) public ResponseEntity handleUserNotAccessibleException( - final UserNotAccessibleException ex + final InvalidUserToChat ex ) { - logger.warn(String.format(EXCEPTION_FORMAT, UserNotAccessibleException.class), ex); + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(new ExceptionResponse(ex.getMessage())); @@ -201,7 +221,7 @@ public ResponseEntity handleUserNotAccessibleException( public ResponseEntity handleWinnerNotFoundException( final WinnerNotFoundException ex ) { - logger.warn(String.format(EXCEPTION_FORMAT, WinnerNotFoundException.class), ex); + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(new ExceptionResponse(ex.getMessage())); @@ -211,47 +231,52 @@ public ResponseEntity handleWinnerNotFoundException( public ResponseEntity handleInvalidAuctionToChatException( final InvalidAuctionToChatException ex ) { - logger.warn(String.format(EXCEPTION_FORMAT, InvalidAuctionToChatException.class), ex); + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(new ExceptionResponse(ex.getMessage())); } @ExceptionHandler(InvalidReporterToAuctionException.class) - public ResponseEntity handleInvalidReporterToAuctionException(final InvalidReporterToAuctionException ex) { - logger.warn(String.format(EXCEPTION_FORMAT, InvalidReporterToAuctionException.class), ex); + public ResponseEntity handleInvalidReporterToAuctionException( + final InvalidReporterToAuctionException ex) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(new ExceptionResponse(ex.getMessage())); } @ExceptionHandler(InvalidReportAuctionException.class) - public ResponseEntity handleInvalidReportAuctionException(final InvalidReportAuctionException ex) { - logger.warn(String.format(EXCEPTION_FORMAT, InvalidReportAuctionException.class), ex); + public ResponseEntity handleInvalidReportAuctionException( + final InvalidReportAuctionException ex) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(new ExceptionResponse(ex.getMessage())); } @ExceptionHandler(AlreadyReportAuctionException.class) - public ResponseEntity handleAlreadyReportAuctionException(final AlreadyReportAuctionException ex) { - logger.warn(String.format(EXCEPTION_FORMAT, AlreadyReportAuctionException.class), ex); + public ResponseEntity handleAlreadyReportAuctionException( + final AlreadyReportAuctionException ex) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(new ExceptionResponse(ex.getMessage())); } - @ExceptionHandler(ChatRoomReportNotAccessibleException.class) - public ResponseEntity handleChatRoomReportNotAccessibleExceptionException(final ChatRoomReportNotAccessibleException ex) { - logger.warn(String.format(EXCEPTION_FORMAT, ChatRoomReportNotAccessibleException.class), ex); + @ExceptionHandler(InvalidChatRoomReportException.class) + public ResponseEntity handleChatRoomReportNotAccessibleExceptionException( + final InvalidChatRoomReportException ex) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(new ExceptionResponse(ex.getMessage())); } @ExceptionHandler(AlreadyReportChatRoomException.class) - public ResponseEntity handleAlreadyReportChatRoomException(final AlreadyReportChatRoomException ex) { - logger.warn(String.format(EXCEPTION_FORMAT, AlreadyReportChatRoomException.class), ex); + public ResponseEntity handleAlreadyReportChatRoomException( + final AlreadyReportChatRoomException ex) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(new ExceptionResponse(ex.getMessage())); @@ -259,7 +284,7 @@ public ResponseEntity handleAlreadyReportChatRoomException(fi @ExceptionHandler(InvalidTokenException.class) public ResponseEntity handleInvalidTokenException(final InvalidTokenException ex) { - logger.warn(String.format(EXCEPTION_FORMAT, InvalidTokenException.class), ex); + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(new ExceptionResponse(ex.getMessage())); @@ -269,7 +294,7 @@ public ResponseEntity handleInvalidTokenException(final Inval public ResponseEntity handleUnsupportedSocialLoginException( final UnsupportedSocialLoginException ex ) { - logger.warn(String.format(EXCEPTION_FORMAT, UnsupportedSocialLoginException.class), ex); + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(new ExceptionResponse(ex.getMessage())); @@ -277,25 +302,123 @@ public ResponseEntity handleUnsupportedSocialLoginException( @ExceptionHandler(UserUnauthorizedException.class) public ResponseEntity handleUserUnauthorizedException(final UserUnauthorizedException ex) { - logger.warn(String.format(EXCEPTION_FORMAT, UserUnauthorizedException.class), ex); + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(new ExceptionResponse(ex.getMessage())); } + @ExceptionHandler(InvalidSearchConditionException.class) + public ResponseEntity handleInvalidSearchConditionException( + final InvalidSearchConditionException ex) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ExceptionResponse(ex.getMessage())); + } + + @ExceptionHandler(NotificationFailedException.class) + public ResponseEntity handleNotificationFailedException(final NotificationFailedException ex) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ExceptionResponse(ex.getMessage())); + } + + @ExceptionHandler(DeviceTokenNotFoundException.class) + public ResponseEntity handleDeviceTokenNotFoundException(final DeviceTokenNotFoundException ex) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); + + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ExceptionResponse(ex.getMessage())); + } + + @ExceptionHandler(AlreadyReviewException.class) + public ResponseEntity handleAlreadyReviewException( + final AlreadyReviewException ex + ) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ExceptionResponse(ex.getMessage())); + } + + @ExceptionHandler(ReviewNotFoundException.class) + public ResponseEntity handleReviewNotFoundException( + final ReviewNotFoundException ex + ) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); + + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ExceptionResponse(ex.getMessage())); + } + + @ExceptionHandler(InvalidAuctionToAskQuestionException.class) + public ResponseEntity handleInvalidAuctionToAskQuestionException(final InvalidAuctionToAskQuestionException ex) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ExceptionResponse(ex.getMessage())); + } + + @ExceptionHandler(InvalidQuestionerException.class) + public ResponseEntity handleInvalidQuestionerException(final InvalidQuestionerException ex) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ExceptionResponse(ex.getMessage())); + } + + @ExceptionHandler(QuestionNotFoundException.class) + public ResponseEntity handleQuestionNotFoundException(final QuestionNotFoundException ex) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); + + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ExceptionResponse(ex.getMessage())); + } + + @ExceptionHandler(AnswerNotFoundException.class) + public ResponseEntity handleAnswerNotFoundException(final AnswerNotFoundException ex) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); + + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ExceptionResponse(ex.getMessage())); + } + + @ExceptionHandler(InvalidAnswererException.class) + public ResponseEntity handleInvalidAnswererException(final InvalidAnswererException ex) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ExceptionResponse(ex.getMessage())); + } + + @ExceptionHandler(InvalidQuestionReportException.class) + public ResponseEntity handleInvalidQuestionReportException(final InvalidQuestionReportException ex) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ExceptionResponse(ex.getMessage())); + } + + @ExceptionHandler(AlreadyAnsweredException.class) + public ResponseEntity handleAlreadyAnsweredException(final AlreadyAnsweredException ex) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ExceptionResponse(ex.getMessage())); + } + @Override protected ResponseEntity handleMethodArgumentNotValid( final MethodArgumentNotValidException ex, - final HttpHeaders headers, - final HttpStatusCode status, - final WebRequest request + final HttpHeaders ignoredHeaders, + final HttpStatusCode ignoredStatus, + final WebRequest ignoredRequest ) { - logger.info(String.format(EXCEPTION_FORMAT, MethodArgumentNotValidException.class), ex); + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); - final String message = ex.getFieldErrors() - .stream() - .map(DefaultMessageSourceResolvable::getDefaultMessage) - .collect(Collectors.joining(System.lineSeparator())); + final String message = ex.getFieldErrors().get(0).getDefaultMessage(); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(new ExceptionResponse(message)); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/application/ImageService.java b/backend/ddang/src/main/java/com/ddang/ddang/image/application/ImageService.java index 2b92197b4..bb64c5dca 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/image/application/ImageService.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/application/ImageService.java @@ -1,9 +1,9 @@ package com.ddang.ddang.image.application; -import com.ddang.ddang.image.application.exception.ImageNotFoundException; import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; import com.ddang.ddang.image.infrastructure.persistence.JpaAuctionImageRepository; -import java.net.MalformedURLException; +import com.ddang.ddang.image.infrastructure.persistence.JpaProfileImageRepository; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; @@ -11,27 +11,47 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.net.MalformedURLException; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class ImageService { + private static final String FILE_PROTOCOL_PREFIX = "file:"; @Value("${image.store.dir}") private String imageStoreDir; + private final JpaProfileImageRepository imageRepository; private final JpaAuctionImageRepository auctionImageRepository; + public Resource readProfileImage(final Long id) throws MalformedURLException { + final ProfileImage profileImage = imageRepository.findById(id) + .orElse(null); + + if (profileImage == null) { + return null; + } + + final String fullPath = findFullPath(profileImage.getImage().getStoreName()); + + return new UrlResource(FILE_PROTOCOL_PREFIX + fullPath); + } + public Resource readAuctionImage(final Long id) throws MalformedURLException { final AuctionImage auctionImage = auctionImageRepository.findById(id) - .orElseThrow(() -> new ImageNotFoundException( - "지정한 이미지를 찾을 수 없습니다." - )); - final String fullPath = findFullPath(auctionImage.getStoreName()); + .orElse(null); + + if (auctionImage == null) { + return null; + } + + final String fullPath = findFullPath(auctionImage.getImage().getStoreName()); - return new UrlResource("file:" + fullPath); + return new UrlResource(FILE_PROTOCOL_PREFIX + fullPath); } - private String findFullPath(String storeImageFileName) { + private String findFullPath(final String storeImageFileName) { return imageStoreDir + storeImageFileName; } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/application/util/ImageIdProcessor.java b/backend/ddang/src/main/java/com/ddang/ddang/image/application/util/ImageIdProcessor.java new file mode 100644 index 000000000..4492fb6f2 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/application/util/ImageIdProcessor.java @@ -0,0 +1,26 @@ +package com.ddang.ddang.image.application.util; + +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; + +public final class ImageIdProcessor { + + private ImageIdProcessor() { + } + + public static Long process(final ProfileImage profileImage) { + if (profileImage == null) { + return null; + } + + return profileImage.getId(); + } + + public static Long process(final AuctionImage auctionImage) { + if (auctionImage == null) { + return null; + } + + return auctionImage.getId(); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/domain/AuctionImage.java b/backend/ddang/src/main/java/com/ddang/ddang/image/domain/AuctionImage.java index 22c0af1cf..dcc81b34b 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/image/domain/AuctionImage.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/domain/AuctionImage.java @@ -1,6 +1,7 @@ package com.ddang.ddang.image.domain; import com.ddang.ddang.auction.domain.Auction; +import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.ForeignKey; @@ -19,16 +20,16 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @EqualsAndHashCode(of = "id") -@ToString(of = {"id", "uploadName", "storeName", "authenticated"}) +@ToString(of = {"id", "image", "authenticated"}) +// TODO: 9/29/23 추후 대표 이미지 구분을 위한 필드 추가 public class AuctionImage { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private String uploadName; - - private String storeName; + @Embedded + private Image image; private boolean authenticated = false; @@ -37,8 +38,7 @@ public class AuctionImage { private Auction auction; public AuctionImage(final String uploadName, final String storeName) { - this.uploadName = uploadName; - this.storeName = storeName; + this.image = new Image(uploadName, storeName); } public void initAuction(final Auction auction) { diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/domain/Image.java b/backend/ddang/src/main/java/com/ddang/ddang/image/domain/Image.java new file mode 100644 index 000000000..bfd2f5dab --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/domain/Image.java @@ -0,0 +1,25 @@ +package com.ddang.ddang.image.domain; + +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@EqualsAndHashCode +@ToString +public class Image { + + private String uploadName; + + private String storeName; + + public Image(final String uploadName, final String storeName) { + this.uploadName = uploadName; + this.storeName = storeName; + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/domain/ProfileImage.java b/backend/ddang/src/main/java/com/ddang/ddang/image/domain/ProfileImage.java new file mode 100644 index 000000000..e45936a8c --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/domain/ProfileImage.java @@ -0,0 +1,33 @@ +package com.ddang.ddang.image.domain; + +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@EqualsAndHashCode(of = "id") +@ToString(of = {"id", "image"}) +public class ProfileImage { + + public static final String DEFAULT_PROFILE_IMAGE_STORE_NAME = "default_profile_image.png"; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Embedded + private Image image; + + public ProfileImage(final String uploadName, final String storeName) { + this.image = new Image(uploadName, storeName); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/domain/StoreImageProcessor.java b/backend/ddang/src/main/java/com/ddang/ddang/image/domain/StoreImageProcessor.java index acccd39f1..19158bfd1 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/image/domain/StoreImageProcessor.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/domain/StoreImageProcessor.java @@ -1,10 +1,13 @@ package com.ddang.ddang.image.domain; import com.ddang.ddang.image.domain.dto.StoreImageDto; -import java.util.List; import org.springframework.web.multipart.MultipartFile; +import java.util.List; + public interface StoreImageProcessor { + StoreImageDto storeImageFile(MultipartFile imageFile); + List storeImageFiles(List imageFiles); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/domain/dto/StoreImageDto.java b/backend/ddang/src/main/java/com/ddang/ddang/image/domain/dto/StoreImageDto.java index 7d04036a6..e265356fb 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/image/domain/dto/StoreImageDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/domain/dto/StoreImageDto.java @@ -1,10 +1,15 @@ package com.ddang.ddang.image.domain.dto; +import com.ddang.ddang.image.domain.ProfileImage; import com.ddang.ddang.image.domain.AuctionImage; public record StoreImageDto(String uploadName, String storeName) { - public AuctionImage toEntity() { + public ProfileImage toProfileImageEntity() { + return new ProfileImage(uploadName, storeName); + } + + public AuctionImage toAuctionImageEntity() { return new AuctionImage(uploadName, storeName); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/local/LocalStoreImageProcessor.java b/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/local/LocalStoreImageProcessor.java index eff3e5765..136b348a4 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/local/LocalStoreImageProcessor.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/local/LocalStoreImageProcessor.java @@ -27,29 +27,29 @@ public class LocalStoreImageProcessor implements StoreImageProcessor { public List storeImageFiles(final List imageFiles) { final List storeImageDtos = new ArrayList<>(); - try { - for (MultipartFile imageFile : imageFiles) { - if (imageFile.isEmpty()) { - throw new EmptyImageException("이미지 파일의 데이터가 비어 있습니다."); - } - - storeImageDtos.add(storeImageFile(imageFile)); + for (MultipartFile imageFile : imageFiles) { + if (imageFile.isEmpty()) { + throw new EmptyImageException("이미지 파일의 데이터가 비어 있습니다."); } - return storeImageDtos; - } catch (IOException e) { - throw new StoreImageFailureException("이미지 저장에 실패했습니다.", e); + storeImageDtos.add(storeImageFile(imageFile)); } + + return storeImageDtos; } - private StoreImageDto storeImageFile(MultipartFile imageFile) throws IOException { - final String originalImageFileName = imageFile.getOriginalFilename(); - final String storeImageFileName = createStoreImageFileName(originalImageFileName); - final String fullPath = findFullPath(storeImageFileName); + public StoreImageDto storeImageFile(MultipartFile imageFile) { + try { + final String originalImageFileName = imageFile.getOriginalFilename(); + final String storeImageFileName = createStoreImageFileName(originalImageFileName); + final String fullPath = findFullPath(storeImageFileName); - imageFile.transferTo(new File(fullPath)); + imageFile.transferTo(new File(fullPath)); - return new StoreImageDto(originalImageFileName, storeImageFileName); + return new StoreImageDto(originalImageFileName, storeImageFileName); + } catch (IOException ex) { + throw new StoreImageFailureException("이미지 저장에 실패했습니다.", ex); + } } private String findFullPath(String storeImageFileName) { diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/persistence/JpaProfileImageRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/persistence/JpaProfileImageRepository.java new file mode 100644 index 000000000..f30774152 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/persistence/JpaProfileImageRepository.java @@ -0,0 +1,13 @@ +package com.ddang.ddang.image.infrastructure.persistence; + +import com.ddang.ddang.image.domain.ProfileImage; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.Optional; + +public interface JpaProfileImageRepository extends JpaRepository { + + @Query("select i from ProfileImage i where i.image.storeName = :storeName") + Optional findByStoreName(final String storeName); +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/ImageController.java b/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/ImageController.java index 7f93b24fb..16ace8049 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/ImageController.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/ImageController.java @@ -18,12 +18,22 @@ public class ImageController { private final ImageService imageService; + @GetMapping("/users/images/{id}") + public ResponseEntity downloadProfileImage(@PathVariable Long id) throws MalformedURLException { + final Resource resource = imageService.readProfileImage(id); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.IMAGE_JPEG); + + return new ResponseEntity<>(resource, headers, HttpStatus.OK); + } + @GetMapping("/auctions/images/{id}") - public ResponseEntity downloadImage(@PathVariable Long id) throws MalformedURLException { + public ResponseEntity downloadAuctionImage(@PathVariable Long id) throws MalformedURLException { final Resource resource = imageService.readAuctionImage(id); HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.IMAGE_PNG); + headers.setContentType(MediaType.IMAGE_JPEG); return new ResponseEntity<>(resource, headers, HttpStatus.OK); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/util/ImageRelativeUrl.java b/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/util/ImageRelativeUrl.java new file mode 100644 index 000000000..7fa5603ea --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/util/ImageRelativeUrl.java @@ -0,0 +1,23 @@ +package com.ddang.ddang.image.presentation.util; + +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +public enum ImageRelativeUrl { + + AUCTION("/auctions/images/"), + USER("/users/images/"); + + private final String value; + + ImageRelativeUrl(final String value) { + this.value = value; + } + + public String calculateAbsoluteUrl() { + final String imageBaseUrl = ServletUriComponentsBuilder.fromCurrentContextPath() + .build() + .toUriString(); + + return imageBaseUrl + value; + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/util/ImageUrlCalculator.java b/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/util/ImageUrlCalculator.java new file mode 100644 index 000000000..f2a0b8759 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/util/ImageUrlCalculator.java @@ -0,0 +1,18 @@ +package com.ddang.ddang.image.presentation.util; + +public final class ImageUrlCalculator { + + private ImageUrlCalculator() { + } + + // TODO: 9/29/23 id 타입을 long으로 변경 및 이미지는 null이 되는 경우가 없도록 할 것 + public static String calculateBy(final ImageRelativeUrl imageRelativeUrl, final Long id) { + final String absoluteUrl = imageRelativeUrl.calculateAbsoluteUrl(); + + return absoluteUrl + id; + } + + public static String calculateBy(final String imageAbsoluteUrl, final Long id) { + return imageAbsoluteUrl + id; + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/notification/application/FcmNotificationService.java b/backend/ddang/src/main/java/com/ddang/ddang/notification/application/FcmNotificationService.java new file mode 100644 index 000000000..8eeb9b6ef --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/notification/application/FcmNotificationService.java @@ -0,0 +1,62 @@ +package com.ddang.ddang.notification.application; + +import com.ddang.ddang.device.application.exception.DeviceTokenNotFoundException; +import com.ddang.ddang.device.domain.DeviceToken; +import com.ddang.ddang.device.infrastructure.persistence.JpaDeviceTokenRepository; +import com.ddang.ddang.notification.application.dto.CreateNotificationDto; +import com.ddang.ddang.notification.domain.NotificationStatus; +import com.google.firebase.messaging.AndroidConfig; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.Message; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.ddang.ddang.notification.application.util.NotificationProperty.BODY; +import static com.ddang.ddang.notification.application.util.NotificationProperty.IMAGE; +import static com.ddang.ddang.notification.application.util.NotificationProperty.NOTIFICATION_TYPE; +import static com.ddang.ddang.notification.application.util.NotificationProperty.REDIRECT_URL; +import static com.ddang.ddang.notification.application.util.NotificationProperty.TITLE; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Slf4j +public class FcmNotificationService implements NotificationService { + + private final FirebaseMessaging firebaseMessaging; + private final JpaDeviceTokenRepository deviceTokenRepository; + + @Override + public NotificationStatus send(final CreateNotificationDto createNotificationDto) throws FirebaseMessagingException { + final DeviceToken deviceToken = deviceTokenRepository.findByUserId(createNotificationDto.targetUserId()) + .orElseThrow(() -> new DeviceTokenNotFoundException( + "사용자의 기기 토큰을 찾을 수 없습니다." + )); + + final String messageId = makeAndSendMessage(createNotificationDto, deviceToken); + return NotificationStatus.calculateStatus(messageId); + } + + private String makeAndSendMessage( + final CreateNotificationDto createNotificationDto, + final DeviceToken deviceToken + ) throws FirebaseMessagingException { + final AndroidConfig androidConfig = AndroidConfig.builder() + .setPriority(AndroidConfig.Priority.HIGH) + .build(); + final Message message = Message.builder() + .setToken(deviceToken.getDeviceToken()) + .putData(NOTIFICATION_TYPE.getKeyName(), createNotificationDto.notificationType().getValue()) + .putData(IMAGE.getKeyName(), createNotificationDto.image()) + .putData(TITLE.getKeyName(), createNotificationDto.title()) + .putData(BODY.getKeyName(), createNotificationDto.body()) + .putData(REDIRECT_URL.getKeyName(), createNotificationDto.redirectUrl()) + .setAndroidConfig(androidConfig) + .build(); + + return firebaseMessaging.send(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/notification/application/NotificationEventListener.java b/backend/ddang/src/main/java/com/ddang/ddang/notification/application/NotificationEventListener.java new file mode 100644 index 000000000..d75990490 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/notification/application/NotificationEventListener.java @@ -0,0 +1,73 @@ +package com.ddang.ddang.notification.application; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.bid.application.dto.BidDto; +import com.ddang.ddang.bid.application.event.BidNotificationEvent; +import com.ddang.ddang.chat.application.dto.MessageDto; +import com.ddang.ddang.chat.application.event.MessageNotificationEvent; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.image.presentation.util.ImageUrlCalculator; +import com.ddang.ddang.notification.application.dto.CreateNotificationDto; +import com.ddang.ddang.notification.domain.NotificationType; +import com.google.firebase.messaging.FirebaseMessagingException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionalEventListener; + +@Component +@RequiredArgsConstructor +@Slf4j +public class NotificationEventListener { + + private static final String URI_DELIMITER = "/"; + private static final String MESSAGE_NOTIFICATION_REDIRECT_URI = "/chattings"; + private static final String BID_NOTIFICATION_REDIRECT_URI = "/auctions"; + private static final String BID_NOTIFICATION_MESSAGE_FORMAT = "상위 입찰자가 나타났습니다. 구매를 원하신다면 더 높은 가격을 제시해 주세요."; + + private final NotificationService notificationService; + + @TransactionalEventListener + public void sendMessageNotification(final MessageNotificationEvent messageNotificationEvent) { + try { + final MessageDto messageDto = messageNotificationEvent.messageDto(); + final ProfileImage profileImage = messageDto.receiver().getProfileImage(); + final CreateNotificationDto createNotificationDto = new CreateNotificationDto( + NotificationType.MESSAGE, + messageDto.receiver().getId(), + messageDto.writer().getName(), + messageDto.contents(), + calculateRedirectUrl(MESSAGE_NOTIFICATION_REDIRECT_URI, messageDto.chatRoom().getId()), + ImageUrlCalculator.calculateBy(messageDto.profileImageAbsoluteUrl(), profileImage.getId()) + ); + notificationService.send(createNotificationDto); + } catch (final FirebaseMessagingException ex) { + log.error("exception type : {}, ", ex.getClass().getSimpleName(), ex); + } + } + + @TransactionalEventListener + public void sendBidNotification(final BidNotificationEvent bidNotificationEvent) { + try { + final BidDto bidDto = bidNotificationEvent.bidDto(); + final Auction auction = bidDto.auctionAndImageDto().auction(); + final AuctionImage auctionImage = bidDto.auctionAndImageDto().auctionImage(); + final CreateNotificationDto createNotificationDto = new CreateNotificationDto( + NotificationType.BID, + bidDto.previousBidderId(), + auction.getTitle(), + BID_NOTIFICATION_MESSAGE_FORMAT, + calculateRedirectUrl(BID_NOTIFICATION_REDIRECT_URI, auction.getId()), + ImageUrlCalculator.calculateBy(bidDto.auctionImageAbsoluteUrl(), auctionImage.getId()) + ); + notificationService.send(createNotificationDto); + } catch (final FirebaseMessagingException ex) { + log.error("exception type : {}, ", ex.getClass().getSimpleName(), ex); + } + } + + private String calculateRedirectUrl(final String uri, final Long id) { + return uri + URI_DELIMITER + id; + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/notification/application/NotificationService.java b/backend/ddang/src/main/java/com/ddang/ddang/notification/application/NotificationService.java new file mode 100644 index 000000000..7c8a93b3b --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/notification/application/NotificationService.java @@ -0,0 +1,12 @@ +package com.ddang.ddang.notification.application; + +import com.ddang.ddang.configuration.fcm.exception.FcmNotFoundException; +import com.ddang.ddang.device.application.exception.DeviceTokenNotFoundException; +import com.ddang.ddang.notification.application.dto.CreateNotificationDto; +import com.ddang.ddang.notification.domain.NotificationStatus; +import com.google.firebase.messaging.FirebaseMessagingException; + +public interface NotificationService { + + NotificationStatus send(final CreateNotificationDto createNotificationDto) throws FirebaseMessagingException, NullPointerException, DeviceTokenNotFoundException, FcmNotFoundException; +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/notification/application/dto/CreateNotificationDto.java b/backend/ddang/src/main/java/com/ddang/ddang/notification/application/dto/CreateNotificationDto.java new file mode 100644 index 000000000..dbf6aabd0 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/notification/application/dto/CreateNotificationDto.java @@ -0,0 +1,25 @@ +package com.ddang.ddang.notification.application.dto; + +import com.ddang.ddang.notification.domain.NotificationType; +import lombok.NonNull; + +public record CreateNotificationDto( + @NonNull + NotificationType notificationType, + + @NonNull + Long targetUserId, + + @NonNull + String title, + + @NonNull + String body, + + @NonNull + String redirectUrl, + + @NonNull + String image +) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/notification/application/exception/NotificationFailedException.java b/backend/ddang/src/main/java/com/ddang/ddang/notification/application/exception/NotificationFailedException.java new file mode 100644 index 000000000..18a7285d8 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/notification/application/exception/NotificationFailedException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.notification.application.exception; + +public class NotificationFailedException extends IllegalStateException { + + public NotificationFailedException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/notification/application/util/NotificationProperty.java b/backend/ddang/src/main/java/com/ddang/ddang/notification/application/util/NotificationProperty.java new file mode 100644 index 000000000..7b8c8ac3a --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/notification/application/util/NotificationProperty.java @@ -0,0 +1,20 @@ +package com.ddang.ddang.notification.application.util; + +import lombok.Getter; + +// TODO: 2023/09/30 안드로이드분들께 image -> imageUrl로 변경 가능한지 여쭤보기 +@Getter +public enum NotificationProperty { + + NOTIFICATION_TYPE("type"), + IMAGE("image"), + TITLE("title"), + BODY("body"), + REDIRECT_URL("redirectUrl"); + + private final String keyName; + + NotificationProperty(final String keyName) { + this.keyName = keyName; + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/notification/domain/NotificationStatus.java b/backend/ddang/src/main/java/com/ddang/ddang/notification/domain/NotificationStatus.java new file mode 100644 index 000000000..9fbef9297 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/notification/domain/NotificationStatus.java @@ -0,0 +1,22 @@ +package com.ddang.ddang.notification.domain; + +import java.util.Objects; + +public enum NotificationStatus { + + SUCCESS(true), + FAIL(false); + + private final boolean isSuccess; + + NotificationStatus(final boolean isSuccess) { + this.isSuccess = isSuccess; + } + + public static NotificationStatus calculateStatus(final String messageId) { + if (Objects.nonNull(messageId)) { + return SUCCESS; + } + return FAIL; + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/notification/domain/NotificationType.java b/backend/ddang/src/main/java/com/ddang/ddang/notification/domain/NotificationType.java new file mode 100644 index 000000000..ba0e6ae1e --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/notification/domain/NotificationType.java @@ -0,0 +1,16 @@ +package com.ddang.ddang.notification.domain; + +import lombok.Getter; + +@Getter +public enum NotificationType { + + MESSAGE("message"), + BID("bid"); + + private final String value; + + NotificationType(final String value) { + this.value = value; + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/application/AnswerService.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/AnswerService.java new file mode 100644 index 000000000..cf1055b8a --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/AnswerService.java @@ -0,0 +1,73 @@ +package com.ddang.ddang.qna.application; + +import com.ddang.ddang.auction.application.exception.UserForbiddenException; +import com.ddang.ddang.qna.application.dto.CreateAnswerDto; +import com.ddang.ddang.qna.application.exception.AlreadyAnsweredException; +import com.ddang.ddang.qna.application.exception.AnswerNotFoundException; +import com.ddang.ddang.qna.application.exception.InvalidAnswererException; +import com.ddang.ddang.qna.application.exception.QuestionNotFoundException; +import com.ddang.ddang.qna.domain.Answer; +import com.ddang.ddang.qna.domain.Question; +import com.ddang.ddang.qna.infrastructure.JpaAnswerRepository; +import com.ddang.ddang.qna.infrastructure.JpaQuestionRepository; +import com.ddang.ddang.user.application.exception.UserNotFoundException; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class AnswerService { + + private final JpaUserRepository userRepository; + private final JpaQuestionRepository questionRepository; + private final JpaAnswerRepository answerRepository; + + @Transactional + public Long create(final CreateAnswerDto answerDto) { + final User writer = userRepository.findById(answerDto.userId()) + .orElseThrow(() -> new UserNotFoundException("해당 사용자를 찾을 수 없습니다.")); + final Question question = questionRepository.findById(answerDto.questionId()) + .orElseThrow(() -> + new QuestionNotFoundException("해당 질문을 찾을 수 없습니다.") + ); + + checkInvalidAnswerer(question, writer); + checkAlreadyAnswered(question); + + final Answer answer = answerDto.toEntity(); + question.addAnswer(answer); + + return answerRepository.save(answer) + .getId(); + } + + private void checkInvalidAnswerer(final Question question, final User writer) { + if (!question.isAnsweringAllowed(writer)) { + throw new InvalidAnswererException("판매자만 답변할 수 있습니다."); + } + } + + private void checkAlreadyAnswered(final Question question) { + if (answerRepository.existsByQuestionId(question.getId())) { + throw new AlreadyAnsweredException("이미 답변한 질문입니다."); + } + } + + @Transactional + public void deleteById(final Long answerId, final Long userId) { + final Answer answer = answerRepository.findByIdAndDeletedIsFalse(answerId) + .orElseThrow(() -> new AnswerNotFoundException("해당 답변을 찾을 수 없습니다.")); + final User user = userRepository.findByIdAndDeletedIsFalse(userId) + .orElseThrow(() -> new UserNotFoundException("해당 사용자를 찾을 수 없습니다.")); + + if (!answer.isWriter(user)) { + throw new UserForbiddenException("삭제할 권한이 없습니다."); + } + + answer.delete(); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/application/QuestionService.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/QuestionService.java new file mode 100644 index 000000000..e2462238f --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/QuestionService.java @@ -0,0 +1,84 @@ +package com.ddang.ddang.qna.application; + +import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; +import com.ddang.ddang.auction.application.exception.UserForbiddenException; +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.qna.application.dto.CreateQuestionDto; +import com.ddang.ddang.qna.application.dto.ReadQnasDto; +import com.ddang.ddang.qna.application.exception.InvalidAuctionToAskQuestionException; +import com.ddang.ddang.qna.application.exception.InvalidQuestionerException; +import com.ddang.ddang.qna.application.exception.QuestionNotFoundException; +import com.ddang.ddang.qna.domain.Question; +import com.ddang.ddang.qna.infrastructure.JpaQuestionRepository; +import com.ddang.ddang.user.application.exception.UserNotFoundException; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class QuestionService { + + private final JpaAuctionRepository auctionRepository; + private final JpaUserRepository userRepository; + private final JpaQuestionRepository questionRepository; + + @Transactional + public Long create(final CreateQuestionDto questionDto) { + final User questioner = userRepository.findByIdAndDeletedIsFalse(questionDto.userId()) + .orElseThrow(() -> new UserNotFoundException("해당 사용자를 찾을 수 없습니다.")); + final Auction auction = auctionRepository.findByIdAndDeletedIsFalse(questionDto.auctionId()) + .orElseThrow(() -> new AuctionNotFoundException("해당 경매를 찾을 수 없습니다.")); + + checkInvalidAuction(auction); + checkInvalidQuestioner(auction, questioner); + + final Question question = questionDto.toEntity(auction, questioner); + + return questionRepository.save(question) + .getId(); + } + + private void checkInvalidAuction(final Auction auction) { + if (auction.isClosed(LocalDateTime.now())) { + throw new InvalidAuctionToAskQuestionException("이미 종료된 경매입니다."); + } + } + + private void checkInvalidQuestioner(final Auction auction, final User questioner) { + if (auction.isOwner(questioner)) { + throw new InvalidQuestionerException("경매 등록자는 질문할 수 없습니다."); + } + } + + public ReadQnasDto readAllByAuctionId(final Long auctionId) { + if (!auctionRepository.existsById(auctionId)) { + throw new AuctionNotFoundException("해당 경매를 찾을 수 없습니다."); + } + + final List questions = questionRepository.findAllByAuctionId(auctionId); + + return ReadQnasDto.from(questions); + } + + @Transactional + public void deleteById(final Long questionId, final Long userId) { + final Question question = questionRepository.findById(questionId) + .orElseThrow(() -> new QuestionNotFoundException("해당 질문을 찾을 수 없습니다.")); + final User user = userRepository.findByIdAndDeletedIsFalse(userId) + .orElseThrow(() -> new UserNotFoundException("해당 사용자를 찾을 수 없습니다.")); + + if (!question.isWriter(user)) { + throw new UserForbiddenException("삭제할 권한이 없습니다."); + } + + question.delete(); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/CreateAnswerDto.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/CreateAnswerDto.java new file mode 100644 index 000000000..79bf745ba --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/CreateAnswerDto.java @@ -0,0 +1,15 @@ +package com.ddang.ddang.qna.application.dto; + +import com.ddang.ddang.qna.presentation.dto.request.CreateAnswerRequest; +import com.ddang.ddang.qna.domain.Answer; + +public record CreateAnswerDto(Long questionId, String content, Long userId) { + + public static CreateAnswerDto of(final Long questionId, final CreateAnswerRequest answerRequest, final Long userId) { + return new CreateAnswerDto(questionId, answerRequest.content(), userId); + } + + public Answer toEntity() { + return new Answer(content); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/CreateQuestionDto.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/CreateQuestionDto.java new file mode 100644 index 000000000..98565e9ef --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/CreateQuestionDto.java @@ -0,0 +1,17 @@ +package com.ddang.ddang.qna.application.dto; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.qna.domain.Question; +import com.ddang.ddang.qna.presentation.dto.request.CreateQuestionRequest; +import com.ddang.ddang.user.domain.User; + +public record CreateQuestionDto(Long auctionId, String content, Long userId) { + + public static CreateQuestionDto of(final CreateQuestionRequest questionRequest, final Long userId) { + return new CreateQuestionDto(questionRequest.auctionId(), questionRequest.content(), userId); + } + + public Question toEntity(final Auction auction, final User questioner) { + return new Question(auction, questioner, content); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/ReadAnswerDto.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/ReadAnswerDto.java new file mode 100644 index 000000000..d7739bf11 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/ReadAnswerDto.java @@ -0,0 +1,22 @@ +package com.ddang.ddang.qna.application.dto; + +import com.ddang.ddang.qna.domain.Answer; +import com.ddang.ddang.user.domain.User; + +import java.time.LocalDateTime; + +public record ReadAnswerDto( + Long id, + ReadUserInQnaDto writerDto, + String content, + LocalDateTime createdTime +) { + public static ReadAnswerDto from(final Answer answer, final User writer) { + return new ReadAnswerDto( + answer.getId(), + ReadUserInQnaDto.from(writer), + answer.getContent(), + answer.getCreatedTime() + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/ReadQnaDto.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/ReadQnaDto.java new file mode 100644 index 000000000..4b99a000a --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/ReadQnaDto.java @@ -0,0 +1,24 @@ +package com.ddang.ddang.qna.application.dto; + +import com.ddang.ddang.qna.domain.Question; + +public record ReadQnaDto( + ReadQuestionDto readQuestionDto, + ReadAnswerDto readAnswerDto +) { + + public static ReadQnaDto from(final Question question) { + final ReadQuestionDto readQuestionDto = ReadQuestionDto.from(question); + final ReadAnswerDto readAnswerDto = processReadAnswerDto(question); + + return new ReadQnaDto(readQuestionDto, readAnswerDto); + } + + private static ReadAnswerDto processReadAnswerDto(final Question question) { + if (question.getAnswer() == null) { + return null; + } + + return ReadAnswerDto.from(question.getAnswer(), question.getAuction().getSeller()); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/ReadQnasDto.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/ReadQnasDto.java new file mode 100644 index 000000000..92d776a5a --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/ReadQnasDto.java @@ -0,0 +1,16 @@ +package com.ddang.ddang.qna.application.dto; + +import com.ddang.ddang.qna.domain.Question; + +import java.util.List; + +public record ReadQnasDto(List readQnaDtos) { + + public static ReadQnasDto from(final List questions) { + final List readQnaDtos = questions.stream() + .map(ReadQnaDto::from) + .toList(); + + return new ReadQnasDto(readQnaDtos); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/ReadQuestionDto.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/ReadQuestionDto.java new file mode 100644 index 000000000..edca5530e --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/ReadQuestionDto.java @@ -0,0 +1,22 @@ +package com.ddang.ddang.qna.application.dto; + +import com.ddang.ddang.qna.domain.Question; + +import java.time.LocalDateTime; + +public record ReadQuestionDto( + Long id, + ReadUserInQnaDto readUserInQnaDto, + String content, + LocalDateTime createdTime +) { + + public static ReadQuestionDto from(final Question question) { + return new ReadQuestionDto( + question.getId(), + ReadUserInQnaDto.from(question.getWriter()), + question.getContent(), + question.getCreatedTime() + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/ReadUserInQnaDto.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/ReadUserInQnaDto.java new file mode 100644 index 000000000..d0a6b2812 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/ReadUserInQnaDto.java @@ -0,0 +1,23 @@ +package com.ddang.ddang.qna.application.dto; + +import com.ddang.ddang.user.domain.User; + +public record ReadUserInQnaDto( + Long id, + String name, + Long profileImageId, + double reliability, + String oauthId, + boolean isDeleted +) { + public static ReadUserInQnaDto from(final User writer) { + return new ReadUserInQnaDto( + writer.getId(), + writer.getName(), + writer.getProfileImage().getId(), + writer.getReliability().getValue(), + writer.getOauthId(), + writer.isDeleted() + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/application/exception/AlreadyAnsweredException.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/exception/AlreadyAnsweredException.java new file mode 100644 index 000000000..a54907393 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/exception/AlreadyAnsweredException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.qna.application.exception; + +public class AlreadyAnsweredException extends IllegalArgumentException { + + public AlreadyAnsweredException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/application/exception/AnswerNotFoundException.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/exception/AnswerNotFoundException.java new file mode 100644 index 000000000..0f898f249 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/exception/AnswerNotFoundException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.qna.application.exception; + +public class AnswerNotFoundException extends IllegalArgumentException { + + public AnswerNotFoundException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/application/exception/InvalidAnswererException.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/exception/InvalidAnswererException.java new file mode 100644 index 000000000..0e67613b5 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/exception/InvalidAnswererException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.qna.application.exception; + +public class InvalidAnswererException extends IllegalArgumentException { + + public InvalidAnswererException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/application/exception/InvalidAuctionToAskQuestionException.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/exception/InvalidAuctionToAskQuestionException.java new file mode 100644 index 000000000..917b08dda --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/exception/InvalidAuctionToAskQuestionException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.qna.application.exception; + +public class InvalidAuctionToAskQuestionException extends IllegalArgumentException { + + public InvalidAuctionToAskQuestionException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/application/exception/InvalidQuestionerException.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/exception/InvalidQuestionerException.java new file mode 100644 index 000000000..5a29d4bd9 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/exception/InvalidQuestionerException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.qna.application.exception; + +public class InvalidQuestionerException extends IllegalArgumentException { + + public InvalidQuestionerException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/application/exception/QuestionNotFoundException.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/exception/QuestionNotFoundException.java new file mode 100644 index 000000000..605380cee --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/exception/QuestionNotFoundException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.qna.application.exception; + +public class QuestionNotFoundException extends IllegalArgumentException { + + public QuestionNotFoundException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/domain/Answer.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/domain/Answer.java new file mode 100644 index 000000000..d21924f5a --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/domain/Answer.java @@ -0,0 +1,62 @@ +package com.ddang.ddang.qna.domain; + +import com.ddang.ddang.common.entity.BaseCreateTimeEntity; +import com.ddang.ddang.user.domain.User; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@EqualsAndHashCode(of = "id", callSuper = false) +@ToString(of = {"id", "content", "deleted"}) +public class Answer extends BaseCreateTimeEntity { + + private static final boolean DELETED_STATUS = true; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "question_id", foreignKey = @ForeignKey(name = "fk_answer_question")) + private Question question; + + @Column(columnDefinition = "text") + private String content; + + @Column(name = "is_deleted") + private boolean deleted = false; + + public Answer(final String content) { + this.content = content; + } + + public void initQuestion(final Question question) { + this.question = question; + } + + public boolean isWriter(final User user) { + return question.getAuction().isOwner(user); + } + + public void delete() { + deleted = DELETED_STATUS; + } + + public User getWriter() { + return question.getAuction().getSeller(); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/domain/Question.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/domain/Question.java new file mode 100644 index 000000000..6043ff435 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/domain/Question.java @@ -0,0 +1,74 @@ +package com.ddang.ddang.qna.domain; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.common.entity.BaseCreateTimeEntity; +import com.ddang.ddang.user.domain.User; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@EqualsAndHashCode(of = "id", callSuper = false) +@ToString(of = {"id", "content", "deleted"}) +public class Question extends BaseCreateTimeEntity { + + private static final boolean DELETED_STATUS = true; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "auction_id", foreignKey = @ForeignKey(name = "fk_question_auction")) + private Auction auction; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "writer_id", foreignKey = @ForeignKey(name = "fk_question_writer")) + private User writer; + + @Column(columnDefinition = "text") + private String content; + + @OneToOne(mappedBy = "question") + private Answer answer; + + @Column(name = "is_deleted") + private boolean deleted = false; + + public Question(final Auction auction, final User writer, final String content) { + this.auction = auction; + this.writer = writer; + this.content = content; + } + + public void addAnswer(final Answer answer) { + this.answer = answer; + answer.initQuestion(this); + } + + public boolean isAnsweringAllowed(final User user) { + return auction.isOwner(user); + } + + public boolean isWriter(final User user) { + return writer.equals(user); + } + + public void delete() { + deleted = DELETED_STATUS; + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/infrastructure/JpaAnswerRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/infrastructure/JpaAnswerRepository.java new file mode 100644 index 000000000..eea851954 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/infrastructure/JpaAnswerRepository.java @@ -0,0 +1,15 @@ +package com.ddang.ddang.qna.infrastructure; + +import com.ddang.ddang.qna.domain.Answer; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface JpaAnswerRepository extends JpaRepository { + + boolean existsByQuestionId(Long questionId); + + @EntityGraph(attributePaths = {"question", "question.auction", "question.auction.seller"}) + Optional findByIdAndDeletedIsFalse(final Long answerId); +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/infrastructure/JpaQuestionRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/infrastructure/JpaQuestionRepository.java new file mode 100644 index 000000000..615e04d2b --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/infrastructure/JpaQuestionRepository.java @@ -0,0 +1,16 @@ +package com.ddang.ddang.qna.infrastructure; + +import com.ddang.ddang.qna.domain.Question; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface JpaQuestionRepository extends JpaRepository { + + Optional findByIdAndDeletedIsFalse(final Long id); + + @EntityGraph(attributePaths = {"writer", "answer", "auction", "auction.seller"}) + List findAllByAuctionId(final Long auctionId); +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/presentation/QnaController.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/presentation/QnaController.java new file mode 100644 index 000000000..184017777 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/presentation/QnaController.java @@ -0,0 +1,75 @@ +package com.ddang.ddang.qna.presentation; + +import com.ddang.ddang.authentication.configuration.AuthenticateUser; +import com.ddang.ddang.authentication.domain.dto.AuthenticationUserInfo; +import com.ddang.ddang.qna.application.AnswerService; +import com.ddang.ddang.qna.application.QuestionService; +import com.ddang.ddang.qna.presentation.dto.request.CreateAnswerRequest; +import com.ddang.ddang.qna.presentation.dto.request.CreateQuestionRequest; +import com.ddang.ddang.qna.application.dto.CreateAnswerDto; +import com.ddang.ddang.qna.application.dto.CreateQuestionDto; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.net.URI; + +@RestController +@RequestMapping("/questions") +@RequiredArgsConstructor +public class QnaController { + + private final QuestionService questionService; + private final AnswerService answerService; + + @PostMapping + public ResponseEntity createQuestion( + @AuthenticateUser AuthenticationUserInfo userInfo, + @RequestBody @Valid final CreateQuestionRequest questionRequest + ) { + questionService.create(CreateQuestionDto.of(questionRequest, userInfo.userId())); + + return ResponseEntity.created(URI.create("/auctions/" + questionRequest.auctionId())) + .build(); + } + + @PostMapping("/{questionId}/answers") + public ResponseEntity createAnswer( + @AuthenticateUser AuthenticationUserInfo userInfo, + @PathVariable final Long questionId, + @RequestBody @Valid final CreateAnswerRequest answerRequest + ) { + answerService.create(CreateAnswerDto.of(questionId, answerRequest, userInfo.userId())); + + return ResponseEntity.created(URI.create("/auctions/" + answerRequest.auctionId())) + .build(); + } + + @DeleteMapping("/{questionId}") + public ResponseEntity deleteQuestion( + @AuthenticateUser AuthenticationUserInfo userInfo, + @PathVariable final Long questionId + ) { + questionService.deleteById(questionId, userInfo.userId()); + + return ResponseEntity.noContent() + .build(); + } + + @DeleteMapping("/answers/{answerId}") + public ResponseEntity deleteAnswer( + @AuthenticateUser AuthenticationUserInfo userInfo, + @PathVariable final Long answerId + ) { + answerService.deleteById(answerId, userInfo.userId()); + + return ResponseEntity.noContent() + .build(); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/presentation/dto/request/CreateAnswerRequest.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/presentation/dto/request/CreateAnswerRequest.java new file mode 100644 index 000000000..f35cc1f08 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/presentation/dto/request/CreateAnswerRequest.java @@ -0,0 +1,15 @@ +package com.ddang.ddang.qna.presentation.dto.request; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; + +public record CreateAnswerRequest( + @NotNull(message = "경매 아이디가 입력되지 않았습니다.") + @Positive(message = "경매 아이디는 양수입니다.") + Long auctionId, + + @NotEmpty(message = "답변이 입력되지 않았습니다.") + String content +) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/presentation/dto/request/CreateQuestionRequest.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/presentation/dto/request/CreateQuestionRequest.java new file mode 100644 index 000000000..95aee34cf --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/presentation/dto/request/CreateQuestionRequest.java @@ -0,0 +1,15 @@ +package com.ddang.ddang.qna.presentation.dto.request; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; + +public record CreateQuestionRequest( + @NotNull(message = "경매 아이디가 입력되지 않았습니다.") + @Positive(message = "경매 아이디는 양수입니다.") + Long auctionId, + + @NotEmpty(message = "질문이 입력되지 않았습니다.") + String content +) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/region/infrastructure/persistence/JpaRegionRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/region/infrastructure/persistence/JpaRegionRepository.java index 4b1ebbc0c..c1f51a653 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/region/infrastructure/persistence/JpaRegionRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/region/infrastructure/persistence/JpaRegionRepository.java @@ -17,8 +17,17 @@ public interface JpaRegionRepository extends JpaRepository { @Query("select r from Region r where r.firstRegion.id = :firstRegionId and r.secondRegion.id = :secondRegionId") List findThirdAllByFirstAndSecondRegionId(final Long firstRegionId, final Long secondRegionId); - @Query("select r from Region r where r.id = :thirdRegionId and " - + "r.firstRegion.id is not null and " - + "r.secondRegion.id is not null") + @Query(""" + select r from Region r where r.id = :thirdRegionId and + r.firstRegion.id is not null and + r.secondRegion.id is not null + """) Optional findThirdRegionById(final Long thirdRegionId); + + @Query(""" + select r from Region r where r.id in :thirdRegionIds and + r.firstRegion.id is not null and + r.secondRegion.id is not null + """) + List findAllThirdRegionByIds(final List thirdRegionIds); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/region/presentation/RegionController.java b/backend/ddang/src/main/java/com/ddang/ddang/region/presentation/RegionController.java index 9769e78cc..2e58a8c30 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/region/presentation/RegionController.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/region/presentation/RegionController.java @@ -3,14 +3,16 @@ import com.ddang.ddang.region.application.RegionService; import com.ddang.ddang.region.application.dto.ReadRegionDto; import com.ddang.ddang.region.presentation.dto.response.ReadRegionResponse; -import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; 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 java.util.List; + @RestController @RequestMapping("/regions") @RequiredArgsConstructor @@ -19,6 +21,7 @@ public class RegionController { private final RegionService regionService; @GetMapping + @Cacheable(cacheNames = "firstRegion") public ResponseEntity> readAllFirst() { final List readRegionDtos = regionService.readAllFirst(); final List readRegionResponses = readRegionDtos.stream() @@ -29,6 +32,7 @@ public ResponseEntity> readAllFirst() { } @GetMapping("/{firstId}") + @Cacheable(cacheNames = "secondRegion") public ResponseEntity> readAllSecond(@PathVariable final Long firstId) { final List readRegionDtos = regionService.readAllSecondByFirstRegionId(firstId); final List readRegionResponses = readRegionDtos.stream() @@ -39,7 +43,8 @@ public ResponseEntity> readAllSecond(@PathVariable fina } @GetMapping("/{firstId}/{secondId}") - public ResponseEntity> readAllSecond( + @Cacheable(cacheNames = "thirdRegion") + public ResponseEntity> readAllThird( @PathVariable final Long firstId, @PathVariable final Long secondId ) { diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/application/AnswerReportService.java b/backend/ddang/src/main/java/com/ddang/ddang/report/application/AnswerReportService.java new file mode 100644 index 000000000..ff402f96b --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/application/AnswerReportService.java @@ -0,0 +1,61 @@ +package com.ddang.ddang.report.application; + +import com.ddang.ddang.qna.application.exception.AnswerNotFoundException; +import com.ddang.ddang.qna.domain.Answer; +import com.ddang.ddang.qna.infrastructure.JpaAnswerRepository; +import com.ddang.ddang.report.application.dto.CreateAnswerReportDto; +import com.ddang.ddang.report.application.dto.ReadAnswerReportDto; +import com.ddang.ddang.report.application.exception.InvalidAnswererReportException; +import com.ddang.ddang.report.domain.AnswerReport; +import com.ddang.ddang.report.infrastructure.persistence.JpaAnswerReportRepository; +import com.ddang.ddang.user.application.exception.UserNotFoundException; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class AnswerReportService { + + private final JpaAnswerRepository answerRepository; + private final JpaUserRepository userRepository; + private final JpaAnswerReportRepository answerReportRepository; + + @Transactional + public Long create(final CreateAnswerReportDto answerReportDto) { + final Answer answer = answerRepository.findByIdAndDeletedIsFalse(answerReportDto.answerId()) + .orElseThrow(() -> + new AnswerNotFoundException("해당 답변을 찾을 수 없습니다.") + ); + final User reporter = userRepository.findByIdAndDeletedIsFalse(answerReportDto.reporterId()) + .orElseThrow(() -> new UserNotFoundException("해당 사용자를 찾을 수 없습니다.")); + checkInvalidAnswerReport(reporter, answer); + + final AnswerReport answerReport = answerReportDto.toEntity(answer, reporter); + + return answerReportRepository.save(answerReport) + .getId(); + } + + private void checkInvalidAnswerReport(final User reporter, final Answer answer) { + if (answer.isWriter(reporter)) { + throw new InvalidAnswererReportException("본인 답변입니다."); + } + if (answerReportRepository.existsByAnswerIdAndReporterId(answer.getId(), reporter.getId())) { + throw new InvalidAnswererReportException("이미 신고한 답변입니다."); + } + } + + public List readAll() { + final List answerReports = answerReportRepository.findAllByOrderByIdAsc(); + + return answerReports.stream() + .map(ReadAnswerReportDto::from) + .toList(); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/application/AuctionReportService.java b/backend/ddang/src/main/java/com/ddang/ddang/report/application/AuctionReportService.java index f69b5a9b5..d38c222f1 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/report/application/AuctionReportService.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/application/AuctionReportService.java @@ -30,7 +30,6 @@ public class AuctionReportService { @Transactional public Long create(final CreateAuctionReportDto auctionReportDto) { - // TODO: 2023/08/08 추후 User 패키지 내에 UserNotFoundException이 생긴다면 해당 예외를 사용하도록 수정 하겠습니다. final User reporter = userRepository.findById(auctionReportDto.reporterId()) .orElseThrow(() -> new UserNotFoundException("해당 사용자를 찾을 수 없습니다.")); final Auction auction = auctionRepository.findById(auctionReportDto.auctionId()) @@ -56,7 +55,7 @@ private void checkInvalidAuctionReport(final User reporter, final Auction auctio } public List readAll() { - final List auctionReports = auctionReportRepository.findAll(); + final List auctionReports = auctionReportRepository.findAllByOrderByIdAsc(); return auctionReports.stream() .map(ReadAuctionReportDto::from) diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/application/ChatRoomReportService.java b/backend/ddang/src/main/java/com/ddang/ddang/report/application/ChatRoomReportService.java index 3b06a3db6..7a3613e79 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/report/application/ChatRoomReportService.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/application/ChatRoomReportService.java @@ -6,7 +6,7 @@ import com.ddang.ddang.report.application.dto.CreateChatRoomReportDto; import com.ddang.ddang.report.application.dto.ReadChatRoomReportDto; import com.ddang.ddang.report.application.exception.AlreadyReportChatRoomException; -import com.ddang.ddang.report.application.exception.ChatRoomReportNotAccessibleException; +import com.ddang.ddang.report.application.exception.InvalidChatRoomReportException; import com.ddang.ddang.report.domain.ChatRoomReport; import com.ddang.ddang.report.infrastructure.persistence.JpaChatRoomReportRepository; import com.ddang.ddang.user.application.exception.UserNotFoundException; @@ -16,7 +16,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; import java.util.List; @Service @@ -28,7 +27,7 @@ public class ChatRoomReportService { private final JpaChatRoomRepository chatRoomRepository; private final JpaChatRoomReportRepository chatRoomReportRepository; - + @Transactional public Long create(final CreateChatRoomReportDto chatRoomReportDto) { final User reporter = userRepository.findById(chatRoomReportDto.reporterId()) .orElseThrow(() -> new UserNotFoundException("해당 사용자를 찾을 수 없습니다.")); @@ -45,7 +44,7 @@ public Long create(final CreateChatRoomReportDto chatRoomReportDto) { private void checkInvalidChatRoomReport(final User reporter, final ChatRoom chatRoom) { if (!chatRoom.isParticipant(reporter)) { - throw new ChatRoomReportNotAccessibleException("해당 채팅방을 신고할 권한이 없습니다."); + throw new InvalidChatRoomReportException("해당 채팅방을 신고할 권한이 없습니다."); } if (chatRoomReportRepository.existsByChatRoomIdAndReporterId(chatRoom.getId(), reporter.getId())) { throw new AlreadyReportChatRoomException("이미 신고한 채팅방입니다."); @@ -53,10 +52,10 @@ private void checkInvalidChatRoomReport(final User reporter, final ChatRoom chat } public List readAll() { - final List auctionReports = chatRoomReportRepository.findAll(); + final List auctionReports = chatRoomReportRepository.findAllByOrderByIdAsc(); return auctionReports.stream() - .map(auctionReport -> ReadChatRoomReportDto.from(auctionReport, LocalDateTime.now())) + .map(ReadChatRoomReportDto::from) .toList(); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/application/QuestionReportService.java b/backend/ddang/src/main/java/com/ddang/ddang/report/application/QuestionReportService.java new file mode 100644 index 000000000..c99192aff --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/application/QuestionReportService.java @@ -0,0 +1,61 @@ +package com.ddang.ddang.report.application; + +import com.ddang.ddang.qna.application.exception.QuestionNotFoundException; +import com.ddang.ddang.qna.domain.Question; +import com.ddang.ddang.qna.infrastructure.JpaQuestionRepository; +import com.ddang.ddang.report.application.dto.CreateQuestionReportDto; +import com.ddang.ddang.report.application.dto.ReadQuestionReportDto; +import com.ddang.ddang.report.application.exception.InvalidQuestionReportException; +import com.ddang.ddang.report.domain.QuestionReport; +import com.ddang.ddang.report.infrastructure.persistence.JpaQuestionReportRepository; +import com.ddang.ddang.user.application.exception.UserNotFoundException; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class QuestionReportService { + + private final JpaQuestionRepository questionRepository; + private final JpaUserRepository userRepository; + private final JpaQuestionReportRepository questionReportRepository; + + @Transactional + public Long create(final CreateQuestionReportDto questionReportDto) { + final Question question = questionRepository.findByIdAndDeletedIsFalse(questionReportDto.questionId()) + .orElseThrow(() -> + new QuestionNotFoundException("해당 질문을 찾을 수 없습니다.") + ); + final User reporter = userRepository.findByIdAndDeletedIsFalse(questionReportDto.reporterId()) + .orElseThrow(() -> new UserNotFoundException("해당 사용자를 찾을 수 없습니다.")); + checkInvalidQuestionReport(reporter, question); + + final QuestionReport questionReport = questionReportDto.toEntity(question, reporter); + + return questionReportRepository.save(questionReport) + .getId(); + } + + private void checkInvalidQuestionReport(final User reporter, final Question question) { + if (question.isWriter(reporter)) { + throw new InvalidQuestionReportException("본인 질문입니다."); + } + if (questionReportRepository.existsByQuestionIdAndReporterId(question.getId(), reporter.getId())) { + throw new InvalidQuestionReportException("이미 신고한 질문입니다."); + } + } + + public List readAll() { + final List questionReports = questionReportRepository.findAllByOrderByIdAsc(); + + return questionReports.stream() + .map(ReadQuestionReportDto::from) + .toList(); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/CreateAnswerReportDto.java b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/CreateAnswerReportDto.java new file mode 100644 index 000000000..6fdfaf890 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/CreateAnswerReportDto.java @@ -0,0 +1,21 @@ +package com.ddang.ddang.report.application.dto; + +import com.ddang.ddang.qna.domain.Answer; +import com.ddang.ddang.report.domain.AnswerReport; +import com.ddang.ddang.report.presentation.dto.request.CreateAnswerReportRequest; +import com.ddang.ddang.user.domain.User; + +public record CreateAnswerReportDto(Long answerId, String description, Long reporterId) { + + public static CreateAnswerReportDto of(final CreateAnswerReportRequest createAnswerReportRequest, final Long userId) { + return new CreateAnswerReportDto( + createAnswerReportRequest.answerId(), + createAnswerReportRequest.description(), + userId + ); + } + + public AnswerReport toEntity(final Answer answer, final User reporter) { + return new AnswerReport(reporter, answer, description); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/CreateQuestionReportDto.java b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/CreateQuestionReportDto.java new file mode 100644 index 000000000..ec724cd12 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/CreateQuestionReportDto.java @@ -0,0 +1,21 @@ +package com.ddang.ddang.report.application.dto; + +import com.ddang.ddang.qna.domain.Question; +import com.ddang.ddang.report.domain.QuestionReport; +import com.ddang.ddang.report.presentation.dto.request.CreateQuestionReportRequest; +import com.ddang.ddang.user.domain.User; + +public record CreateQuestionReportDto(Long questionId, String description, Long reporterId) { + + public static CreateQuestionReportDto of(final CreateQuestionReportRequest questionReportRequest, final Long userId) { + return new CreateQuestionReportDto( + questionReportRequest.questionId(), + questionReportRequest.description(), + userId + ); + } + + public QuestionReport toEntity(final Question question, final User reporter) { + return new QuestionReport(reporter, question, description); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadAnswerInReportDto.java b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadAnswerInReportDto.java new file mode 100644 index 000000000..90bc64be9 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadAnswerInReportDto.java @@ -0,0 +1,22 @@ +package com.ddang.ddang.report.application.dto; + +import com.ddang.ddang.qna.domain.Answer; + +import java.time.LocalDateTime; + +public record ReadAnswerInReportDto( + Long id, + ReadUserInReportDto userDto, + String content, + LocalDateTime createdTime +) { + + public static ReadAnswerInReportDto from(final Answer answer) { + return new ReadAnswerInReportDto( + answer.getId(), + ReadUserInReportDto.from(answer.getWriter()), + answer.getContent(), + answer.getCreatedTime() + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadAnswerReportDto.java b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadAnswerReportDto.java new file mode 100644 index 000000000..402e323c9 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadAnswerReportDto.java @@ -0,0 +1,24 @@ +package com.ddang.ddang.report.application.dto; + +import com.ddang.ddang.report.domain.AnswerReport; + +import java.time.LocalDateTime; + +public record ReadAnswerReportDto( + Long id, + ReadReporterDto reporterDto, + LocalDateTime createdTime, + ReadAnswerInReportDto answerDto, + String description +) { + + public static ReadAnswerReportDto from(final AnswerReport answerReport) { + return new ReadAnswerReportDto( + answerReport.getId(), + ReadReporterDto.from(answerReport.getReporter()), + answerReport.getCreatedTime(), + ReadAnswerInReportDto.from(answerReport.getAnswer()), + answerReport.getDescription() + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadChatRoomInReportDto.java b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadChatRoomInReportDto.java index d552e62bd..bd444659a 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadChatRoomInReportDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadChatRoomInReportDto.java @@ -2,21 +2,17 @@ import com.ddang.ddang.chat.domain.ChatRoom; -import java.time.LocalDateTime; - public record ReadChatRoomInReportDto( Long id, ReadAuctionInReportDto auctionDto, - ReadUserInReportDto partnerDto, - boolean isChatAvailable + ReadUserInReportDto partnerDto ) { - public static ReadChatRoomInReportDto from(final ChatRoom chatRoom, final LocalDateTime targetTime) { + public static ReadChatRoomInReportDto from(final ChatRoom chatRoom) { return new ReadChatRoomInReportDto( chatRoom.getId(), ReadAuctionInReportDto.from(chatRoom.getAuction()), - ReadUserInReportDto.from(chatRoom.getBuyer()), - chatRoom.isChatAvailableTime(targetTime) + ReadUserInReportDto.from(chatRoom.getBuyer()) ); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadChatRoomReportDto.java b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadChatRoomReportDto.java index 7bbb8b354..4cbe51c9e 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadChatRoomReportDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadChatRoomReportDto.java @@ -12,12 +12,12 @@ public record ReadChatRoomReportDto( String description ) { - public static ReadChatRoomReportDto from(final ChatRoomReport chatRoomReport, final LocalDateTime targetTime) { + public static ReadChatRoomReportDto from(final ChatRoomReport chatRoomReport) { return new ReadChatRoomReportDto( chatRoomReport.getId(), ReadReporterDto.from(chatRoomReport.getReporter()), chatRoomReport.getCreatedTime(), - ReadChatRoomInReportDto.from(chatRoomReport.getChatRoom(), targetTime), + ReadChatRoomInReportDto.from(chatRoomReport.getChatRoom()), chatRoomReport.getDescription() ); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadQuestionInReportDto.java b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadQuestionInReportDto.java new file mode 100644 index 000000000..a44add06b --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadQuestionInReportDto.java @@ -0,0 +1,22 @@ +package com.ddang.ddang.report.application.dto; + +import com.ddang.ddang.qna.domain.Question; + +import java.time.LocalDateTime; + +public record ReadQuestionInReportDto( + Long id, + ReadUserInReportDto userDto, + String content, + LocalDateTime createdTime +) { + + public static ReadQuestionInReportDto from(final Question question) { + return new ReadQuestionInReportDto( + question.getId(), + ReadUserInReportDto.from(question.getWriter()), + question.getContent(), + question.getCreatedTime() + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadQuestionReportDto.java b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadQuestionReportDto.java new file mode 100644 index 000000000..8b1f79018 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadQuestionReportDto.java @@ -0,0 +1,24 @@ +package com.ddang.ddang.report.application.dto; + +import com.ddang.ddang.report.domain.QuestionReport; + +import java.time.LocalDateTime; + +public record ReadQuestionReportDto( + Long id, + ReadReporterDto reporterDto, + LocalDateTime createdTime, + ReadQuestionInReportDto questionDto, + String description +) { + + public static ReadQuestionReportDto from(final QuestionReport questionReport) { + return new ReadQuestionReportDto( + questionReport.getId(), + ReadReporterDto.from(questionReport.getReporter()), + questionReport.getCreatedTime(), + ReadQuestionInReportDto.from(questionReport.getQuestion()), + questionReport.getDescription() + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadReporterDto.java b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadReporterDto.java index 602775584..09e21af9e 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadReporterDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadReporterDto.java @@ -1,15 +1,17 @@ package com.ddang.ddang.report.application.dto; +import com.ddang.ddang.image.application.util.ImageIdProcessor; import com.ddang.ddang.user.domain.User; -public record ReadReporterDto(Long id, String name, String profileImage, double reliability) { +public record ReadReporterDto(Long id, String name, Long profileImageId, double reliability, boolean isDeleted) { public static ReadReporterDto from(final User reporter) { return new ReadReporterDto( reporter.getId(), reporter.getName(), - reporter.getProfileImage(), - reporter.getReliability() + ImageIdProcessor.process(reporter.getProfileImage()), + reporter.getReliability().getValue(), + reporter.isDeleted() ); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadUserInReportDto.java b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadUserInReportDto.java index bac59999a..dbc8db994 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadUserInReportDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadUserInReportDto.java @@ -1,16 +1,25 @@ package com.ddang.ddang.report.application.dto; +import com.ddang.ddang.image.application.util.ImageIdProcessor; import com.ddang.ddang.user.domain.User; -public record ReadUserInReportDto(Long id, String name, String profileImage, double reliability, String oauthId) { +public record ReadUserInReportDto( + Long id, + String name, + Long profileImageId, + double reliability, + String oauthId, + boolean isSellerDeleted +) { public static ReadUserInReportDto from(final User user) { return new ReadUserInReportDto( user.getId(), user.getName(), - user.getProfileImage(), - user.getReliability(), - user.getOauthId() + ImageIdProcessor.process(user.getProfileImage()), + user.getReliability().getValue(), + user.getOauthId(), + user.isDeleted() ); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/application/exception/ChatRoomReportNotAccessibleException.java b/backend/ddang/src/main/java/com/ddang/ddang/report/application/exception/ChatRoomReportNotAccessibleException.java deleted file mode 100644 index dd7c4f274..000000000 --- a/backend/ddang/src/main/java/com/ddang/ddang/report/application/exception/ChatRoomReportNotAccessibleException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.ddang.ddang.report.application.exception; - -public class ChatRoomReportNotAccessibleException extends IllegalArgumentException { - - public ChatRoomReportNotAccessibleException(final String message) { - super(message); - } -} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/application/exception/InvalidAnswererReportException.java b/backend/ddang/src/main/java/com/ddang/ddang/report/application/exception/InvalidAnswererReportException.java new file mode 100644 index 000000000..a2642a876 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/application/exception/InvalidAnswererReportException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.report.application.exception; + +public class InvalidAnswererReportException extends IllegalArgumentException { + + public InvalidAnswererReportException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/application/exception/InvalidChatRoomReportException.java b/backend/ddang/src/main/java/com/ddang/ddang/report/application/exception/InvalidChatRoomReportException.java new file mode 100644 index 000000000..e52fcb1e7 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/application/exception/InvalidChatRoomReportException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.report.application.exception; + +public class InvalidChatRoomReportException extends IllegalArgumentException { + + public InvalidChatRoomReportException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/application/exception/InvalidQuestionReportException.java b/backend/ddang/src/main/java/com/ddang/ddang/report/application/exception/InvalidQuestionReportException.java new file mode 100644 index 000000000..22e3dc5b8 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/application/exception/InvalidQuestionReportException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.report.application.exception; + +public class InvalidQuestionReportException extends IllegalArgumentException { + + public InvalidQuestionReportException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/domain/AnswerReport.java b/backend/ddang/src/main/java/com/ddang/ddang/report/domain/AnswerReport.java new file mode 100644 index 000000000..0888c5982 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/domain/AnswerReport.java @@ -0,0 +1,48 @@ +package com.ddang.ddang.report.domain; + +import com.ddang.ddang.common.entity.BaseCreateTimeEntity; +import com.ddang.ddang.qna.domain.Answer; +import com.ddang.ddang.user.domain.User; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@EqualsAndHashCode(of = "id") +@ToString(of = {"id", "description"}) +public class AnswerReport extends BaseCreateTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "reporter_id", foreignKey = @ForeignKey(name = "fk_answer_report_reporter")) + private User reporter; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "answer_id", foreignKey = @ForeignKey(name = "fk_answer_report_answer")) + private Answer answer; + + @Column(columnDefinition = "text") + private String description; + + public AnswerReport(final User reporter, final Answer answer, final String description) { + this.reporter = reporter; + this.answer = answer; + this.description = description; + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/domain/QuestionReport.java b/backend/ddang/src/main/java/com/ddang/ddang/report/domain/QuestionReport.java new file mode 100644 index 000000000..1feea6a4a --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/domain/QuestionReport.java @@ -0,0 +1,48 @@ +package com.ddang.ddang.report.domain; + +import com.ddang.ddang.common.entity.BaseCreateTimeEntity; +import com.ddang.ddang.qna.domain.Question; +import com.ddang.ddang.user.domain.User; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@EqualsAndHashCode(of = "id") +@ToString(of = {"id", "description"}) +public class QuestionReport extends BaseCreateTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "reporter_id", foreignKey = @ForeignKey(name = "fk_question_report_reporter")) + private User reporter; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "question_id", foreignKey = @ForeignKey(name = "fk_question_report_question")) + private Question question; + + @Column(columnDefinition = "text") + private String description; + + public QuestionReport(final User reporter, final Question question, final String description) { + this.reporter = reporter; + this.question = question; + this.description = description; + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaAnswerReportRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaAnswerReportRepository.java new file mode 100644 index 000000000..46dcac184 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaAnswerReportRepository.java @@ -0,0 +1,15 @@ +package com.ddang.ddang.report.infrastructure.persistence; + +import com.ddang.ddang.report.domain.AnswerReport; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface JpaAnswerReportRepository extends JpaRepository { + + boolean existsByAnswerIdAndReporterId(final Long answerId, final Long ReporterId); + + @EntityGraph(attributePaths = {"reporter", "answer", "answer.question", "answer.question.auction", "answer.question.auction.seller"}) + List findAllByOrderByIdAsc(); +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaAuctionReportRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaAuctionReportRepository.java index 307d2c296..c426a4051 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaAuctionReportRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaAuctionReportRepository.java @@ -10,7 +10,6 @@ public interface JpaAuctionReportRepository extends JpaRepository findAll(); + List findAllByOrderByIdAsc(); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaChatRoomReportRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaChatRoomReportRepository.java index 9cd667057..d6965e7df 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaChatRoomReportRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaChatRoomReportRepository.java @@ -10,7 +10,6 @@ public interface JpaChatRoomReportRepository extends JpaRepository findAll(); + List findAllByOrderByIdAsc(); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaQuestionReportRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaQuestionReportRepository.java new file mode 100644 index 000000000..6f5e7006a --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaQuestionReportRepository.java @@ -0,0 +1,15 @@ +package com.ddang.ddang.report.infrastructure.persistence; + +import com.ddang.ddang.report.domain.QuestionReport; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface JpaQuestionReportRepository extends JpaRepository { + + boolean existsByQuestionIdAndReporterId(final Long questionId, final Long reporterId); + + @EntityGraph(attributePaths = {"reporter", "question"}) + List findAllByOrderByIdAsc(); +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/ReportController.java b/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/ReportController.java index 98c24cb7d..638c2e609 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/ReportController.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/ReportController.java @@ -2,16 +2,26 @@ import com.ddang.ddang.authentication.configuration.AuthenticateUser; import com.ddang.ddang.authentication.domain.dto.AuthenticationUserInfo; +import com.ddang.ddang.report.application.AnswerReportService; import com.ddang.ddang.report.application.AuctionReportService; import com.ddang.ddang.report.application.ChatRoomReportService; +import com.ddang.ddang.report.application.QuestionReportService; +import com.ddang.ddang.report.application.dto.CreateAnswerReportDto; import com.ddang.ddang.report.application.dto.CreateAuctionReportDto; import com.ddang.ddang.report.application.dto.CreateChatRoomReportDto; +import com.ddang.ddang.report.application.dto.CreateQuestionReportDto; +import com.ddang.ddang.report.application.dto.ReadAnswerReportDto; import com.ddang.ddang.report.application.dto.ReadAuctionReportDto; import com.ddang.ddang.report.application.dto.ReadChatRoomReportDto; +import com.ddang.ddang.report.application.dto.ReadQuestionReportDto; +import com.ddang.ddang.report.presentation.dto.request.CreateAnswerReportRequest; import com.ddang.ddang.report.presentation.dto.request.CreateAuctionReportRequest; import com.ddang.ddang.report.presentation.dto.request.CreateChatRoomReportRequest; +import com.ddang.ddang.report.presentation.dto.request.CreateQuestionReportRequest; +import com.ddang.ddang.report.presentation.dto.response.ReadAnswerReportsResponse; import com.ddang.ddang.report.presentation.dto.response.ReadAuctionReportsResponse; import com.ddang.ddang.report.presentation.dto.response.ReadChatRoomReportsResponse; +import com.ddang.ddang.report.presentation.dto.response.ReadQuestionReportsResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -31,9 +41,11 @@ public class ReportController { private final AuctionReportService auctionReportService; private final ChatRoomReportService chatRoomReportService; + private final QuestionReportService questionReportService; + private final AnswerReportService answerReportService; @PostMapping("/auctions") - public ResponseEntity createAuctinReport( + public ResponseEntity createAuctionReport( @AuthenticateUser final AuthenticationUserInfo userInfo, @RequestBody @Valid final CreateAuctionReportRequest auctionReportRequest ) { @@ -69,4 +81,42 @@ public ResponseEntity readAllChatRoomReport() { return ResponseEntity.ok(response); } + + @PostMapping("/questions") + public ResponseEntity createQuestionReport( + @AuthenticateUser final AuthenticationUserInfo userInfo, + @RequestBody @Valid final CreateQuestionReportRequest createQuestionReportRequest + ) { + questionReportService.create(CreateQuestionReportDto.of(createQuestionReportRequest, userInfo.userId())); + + return ResponseEntity.created(URI.create("/auctions/" + createQuestionReportRequest.auctionId() + "/questions")) + .build(); + } + + @GetMapping("/questions") + public ResponseEntity readAllQuestionReport() { + final List readQuestionReportDtos = questionReportService.readAll(); + final ReadQuestionReportsResponse response = ReadQuestionReportsResponse.from(readQuestionReportDtos); + + return ResponseEntity.ok(response); + } + + @PostMapping("/answers") + public ResponseEntity createAnswerReport( + @AuthenticateUser final AuthenticationUserInfo userInfo, + @RequestBody @Valid final CreateAnswerReportRequest createAnswerReportRequest + ) { + answerReportService.create(CreateAnswerReportDto.of(createAnswerReportRequest, userInfo.userId())); + + return ResponseEntity.created(URI.create("/auctions/" + createAnswerReportRequest.auctionId() + "/questions")) + .build(); + } + + @GetMapping("/answers") + public ResponseEntity readAllAnswerReport() { + final List readAnswerReportDtos = answerReportService.readAll(); + final ReadAnswerReportsResponse response = ReadAnswerReportsResponse.from(readAnswerReportDtos); + + return ResponseEntity.ok(response); + } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/request/CreateAnswerReportRequest.java b/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/request/CreateAnswerReportRequest.java new file mode 100644 index 000000000..65b5c2410 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/request/CreateAnswerReportRequest.java @@ -0,0 +1,19 @@ +package com.ddang.ddang.report.presentation.dto.request; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; + +public record CreateAnswerReportRequest( + @NotNull(message = "경매 아이디가 입력되지 않았습니다.") + @Positive(message = "경매 아이디는 양수여야 합니다.") + Long auctionId, + + @NotNull(message = "질문 아이디가 입력되지 않았습니다.") + @Positive(message = "질문 아이디는 양수여야 합니다.") + Long answerId, + + @NotEmpty(message = "신고 내용이 입력되지 않았습니다.") + String description +) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/request/CreateQuestionReportRequest.java b/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/request/CreateQuestionReportRequest.java new file mode 100644 index 000000000..c9aaef59d --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/request/CreateQuestionReportRequest.java @@ -0,0 +1,19 @@ +package com.ddang.ddang.report.presentation.dto.request; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; + +public record CreateQuestionReportRequest( + @NotNull(message = "경매 아이디가 입력되지 않았습니다.") + @Positive(message = "경매 아이디는 양수여야 합니다.") + Long auctionId, + + @NotNull(message = "질문 아이디가 입력되지 않았습니다.") + @Positive(message = "질문 아이디는 양수여야 합니다.") + Long questionId, + + @NotEmpty(message = "신고 내용이 입력되지 않았습니다.") + String description +) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadAnswerInReportResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadAnswerInReportResponse.java new file mode 100644 index 000000000..59980d8fb --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadAnswerInReportResponse.java @@ -0,0 +1,10 @@ +package com.ddang.ddang.report.presentation.dto.response; + +import com.ddang.ddang.report.application.dto.ReadAnswerInReportDto; + +public record ReadAnswerInReportResponse(Long id) { + + public static ReadAnswerInReportResponse from(final ReadAnswerInReportDto answerDto) { + return new ReadAnswerInReportResponse(answerDto.id()); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadAnswerReportResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadAnswerReportResponse.java new file mode 100644 index 000000000..12b6d4152 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadAnswerReportResponse.java @@ -0,0 +1,30 @@ +package com.ddang.ddang.report.presentation.dto.response; + +import com.ddang.ddang.report.application.dto.ReadAnswerReportDto; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDateTime; + +public record ReadAnswerReportResponse( + Long id, + + ReadReporterResponse reporter, + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul") + LocalDateTime createdTime, + + ReadAnswerInReportResponse answer, + + String description +) { + + public static ReadAnswerReportResponse from(final ReadAnswerReportDto readAnswerReportDto) { + return new ReadAnswerReportResponse( + readAnswerReportDto.id(), + ReadReporterResponse.from(readAnswerReportDto.reporterDto()), + readAnswerReportDto.createdTime(), + ReadAnswerInReportResponse.from(readAnswerReportDto.answerDto()), + readAnswerReportDto.description() + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadAnswerReportsResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadAnswerReportsResponse.java new file mode 100644 index 000000000..de940621b --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadAnswerReportsResponse.java @@ -0,0 +1,16 @@ +package com.ddang.ddang.report.presentation.dto.response; + +import com.ddang.ddang.report.application.dto.ReadAnswerReportDto; + +import java.util.List; + +public record ReadAnswerReportsResponse(List reports) { + + public static ReadAnswerReportsResponse from(final List readAnswerReportDtos) { + final List reportResponses = readAnswerReportDtos.stream() + .map(ReadAnswerReportResponse::from) + .toList(); + + return new ReadAnswerReportsResponse(reportResponses); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadQuestionInReportResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadQuestionInReportResponse.java new file mode 100644 index 000000000..b8ca0eb5f --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadQuestionInReportResponse.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.report.presentation.dto.response; + +public record ReadQuestionInReportResponse(Long id) { + + public static ReadQuestionInReportResponse from(final Long id) { + return new ReadQuestionInReportResponse(id); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadQuestionReportResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadQuestionReportResponse.java new file mode 100644 index 000000000..33984d6f8 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadQuestionReportResponse.java @@ -0,0 +1,30 @@ +package com.ddang.ddang.report.presentation.dto.response; + +import com.ddang.ddang.report.application.dto.ReadQuestionReportDto; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDateTime; + +public record ReadQuestionReportResponse( + Long id, + + ReadReporterResponse reporter, + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul") + LocalDateTime createdTime, + + ReadQuestionInReportResponse question, + + String description +) { + + public static ReadQuestionReportResponse from(final ReadQuestionReportDto readQuestionReportDto) { + return new ReadQuestionReportResponse( + readQuestionReportDto.id(), + ReadReporterResponse.from(readQuestionReportDto.reporterDto()), + readQuestionReportDto.createdTime(), + ReadQuestionInReportResponse.from(readQuestionReportDto.questionDto().id()), + readQuestionReportDto.description() + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadQuestionReportsResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadQuestionReportsResponse.java new file mode 100644 index 000000000..1aed1d11f --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadQuestionReportsResponse.java @@ -0,0 +1,16 @@ +package com.ddang.ddang.report.presentation.dto.response; + +import com.ddang.ddang.report.application.dto.ReadQuestionReportDto; + +import java.util.List; + +public record ReadQuestionReportsResponse(List reports) { + + public static ReadQuestionReportsResponse from(final List questionReportDtos) { + final List reportsResponse = questionReportDtos.stream() + .map(ReadQuestionReportResponse::from) + .toList(); + + return new ReadQuestionReportsResponse(reportsResponse); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadReporterResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadReporterResponse.java index 6b656a677..7cc03e1a0 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadReporterResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/presentation/dto/response/ReadReporterResponse.java @@ -1,10 +1,12 @@ package com.ddang.ddang.report.presentation.dto.response; import com.ddang.ddang.report.application.dto.ReadReporterDto; +import com.ddang.ddang.user.presentation.util.NameProcessor; public record ReadReporterResponse(Long id, String name) { public static ReadReporterResponse from(final ReadReporterDto reporterDto) { - return new ReadReporterResponse(reporterDto.id(), reporterDto.name()); + final String name = NameProcessor.process(reporterDto.isDeleted(), reporterDto.name()); + return new ReadReporterResponse(reporterDto.id(), name); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/review/application/ReviewService.java b/backend/ddang/src/main/java/com/ddang/ddang/review/application/ReviewService.java new file mode 100644 index 000000000..fad0e2a06 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/review/application/ReviewService.java @@ -0,0 +1,95 @@ +package com.ddang.ddang.review.application; + +import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.review.application.dto.CreateReviewDto; +import com.ddang.ddang.review.application.dto.ReadReviewDetailDto; +import com.ddang.ddang.review.application.dto.ReadReviewDto; +import com.ddang.ddang.review.application.exception.AlreadyReviewException; +import com.ddang.ddang.review.application.exception.InvalidUserToReview; +import com.ddang.ddang.review.application.exception.ReviewNotFoundException; +import com.ddang.ddang.review.domain.Review; +import com.ddang.ddang.review.infrastructure.persistence.JpaReviewRepository; +import com.ddang.ddang.user.application.exception.UserNotFoundException; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class ReviewService { + + private final JpaReviewRepository reviewRepository; + private final JpaAuctionRepository auctionRepository; + private final JpaUserRepository userRepository; + + @Transactional + public Long create(final CreateReviewDto reviewDto) { + final Auction findAuction = auctionRepository.findById(reviewDto.auctionId()) + .orElseThrow(() -> + new AuctionNotFoundException("해당 경매를 찾을 수 없습니다.") + ); + final User writer = userRepository.findById(reviewDto.writerId()) + .orElseThrow(() -> new UserNotFoundException("작성자 정보를 찾을 수 없습니다.")); + final User target = userRepository.findById(reviewDto.targetId()) + .orElseThrow(() -> new UserNotFoundException("평가 상대의 정보를 찾을 수 없습니다.")); + + validateWriterCanReview(findAuction, writer); + + final Review review = reviewDto.toEntity(findAuction, writer, target); + final Review persistReview = saveReviewAndUpdateReliability(review, target); + + return persistReview.getId(); + } + + private void validateWriterCanReview(final Auction auction, final User writer) { + if (!auction.isSellerOrWinner(writer, LocalDateTime.now())) { + throw new InvalidUserToReview("경매의 판매자 또는 최종 낙찰자만 평가가 가능합니다."); + } + + validateAlreadyReviewed(auction, writer); + } + + private void validateAlreadyReviewed(final Auction auction, final User writer) { + if (reviewRepository.existsByAuctionIdAndWriterId(auction.getId(), writer.getId())) { + throw new AlreadyReviewException("이미 평가하였습니다."); + } + } + + private Review saveReviewAndUpdateReliability(final Review review, final User target) { + final Review persistReview = reviewRepository.save(review); + + final List targetReviews = reviewRepository.findAllByTargetId(target.getId()); + target.updateReliability(targetReviews); + + return persistReview; + } + + public ReadReviewDetailDto readByReviewId(final Long reviewId) { + final Review findReview = reviewRepository.findById(reviewId) + .orElseThrow(() -> new ReviewNotFoundException("해당 평가를 찾을 수 없습니다.")); + + return ReadReviewDetailDto.from(findReview); + } + + public List readAllByTargetId(final Long targetId) { + final List targetReviews = reviewRepository.findAllByTargetId(targetId); + + return targetReviews.stream() + .map(ReadReviewDto::from) + .toList(); + } + + public ReadReviewDetailDto readByAuctionIdAndWriterId(final Long writerId, final Long auctionId) { + return reviewRepository.findByAuctionIdAndWriterId(auctionId, writerId) + .map(ReadReviewDetailDto::from) + .orElse(ReadReviewDetailDto.EMPTY); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/review/application/dto/CreateReviewDto.java b/backend/ddang/src/main/java/com/ddang/ddang/review/application/dto/CreateReviewDto.java new file mode 100644 index 000000000..888ecb3d5 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/review/application/dto/CreateReviewDto.java @@ -0,0 +1,30 @@ +package com.ddang.ddang.review.application.dto; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.review.domain.Review; +import com.ddang.ddang.review.domain.Score; +import com.ddang.ddang.review.presentation.dto.request.CreateReviewRequest; +import com.ddang.ddang.user.domain.User; + +public record CreateReviewDto(Long auctionId, Long writerId, Long targetId, String content, Double score) { + + public static CreateReviewDto of(final Long writerId, final CreateReviewRequest createReviewRequest) { + return new CreateReviewDto( + createReviewRequest.auctionId(), + writerId, + createReviewRequest.targetId(), + createReviewRequest.content(), + createReviewRequest.score().doubleValue() + ); + } + + public Review toEntity(final Auction auction, final User writer, final User target) { + return Review.builder() + .auction(auction) + .writer(writer) + .target(target) + .content(content) + .score(new Score(score)) + .build(); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/review/application/dto/ReadReviewDetailDto.java b/backend/ddang/src/main/java/com/ddang/ddang/review/application/dto/ReadReviewDetailDto.java new file mode 100644 index 000000000..16dc878e9 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/review/application/dto/ReadReviewDetailDto.java @@ -0,0 +1,16 @@ +package com.ddang.ddang.review.application.dto; + +import com.ddang.ddang.review.domain.Review; + +import javax.annotation.Nullable; + +public record ReadReviewDetailDto(@Nullable Double score, @Nullable String content) { + + private static final Double EMPTY_SCORE = null; + private static final String EMPTY_CONTENT = null; + public static final ReadReviewDetailDto EMPTY = new ReadReviewDetailDto(EMPTY_SCORE, EMPTY_CONTENT); + + public static ReadReviewDetailDto from(final Review review) { + return new ReadReviewDetailDto(review.getScore().getValue(), review.getContent()); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/review/application/dto/ReadReviewDto.java b/backend/ddang/src/main/java/com/ddang/ddang/review/application/dto/ReadReviewDto.java new file mode 100644 index 000000000..becaf5d27 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/review/application/dto/ReadReviewDto.java @@ -0,0 +1,24 @@ +package com.ddang.ddang.review.application.dto; + +import com.ddang.ddang.review.domain.Review; + +import java.time.LocalDateTime; + +public record ReadReviewDto( + Long id, + ReadUserInReviewDto writer, + String content, + Double score, + LocalDateTime createdTime +) { + + public static ReadReviewDto from(final Review review) { + return new ReadReviewDto( + review.getId(), + ReadUserInReviewDto.from(review.getWriter()), + review.getContent(), + review.getScore().getValue(), + review.getCreatedTime() + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/review/application/dto/ReadUserInReviewDto.java b/backend/ddang/src/main/java/com/ddang/ddang/review/application/dto/ReadUserInReviewDto.java new file mode 100644 index 000000000..ecbf938e8 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/review/application/dto/ReadUserInReviewDto.java @@ -0,0 +1,16 @@ +package com.ddang.ddang.review.application.dto; + +import com.ddang.ddang.user.domain.User; + +public record ReadUserInReviewDto(Long id, String name, Long profileImageId, double reliability, String oauthId) { + + public static ReadUserInReviewDto from(final User user) { + return new ReadUserInReviewDto( + user.getId(), + user.getName(), + user.getProfileImage().getId(), + user.getReliability().getValue(), + user.getOauthId() + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/review/application/exception/AlreadyReviewException.java b/backend/ddang/src/main/java/com/ddang/ddang/review/application/exception/AlreadyReviewException.java new file mode 100644 index 000000000..2a51263dc --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/review/application/exception/AlreadyReviewException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.review.application.exception; + +public class AlreadyReviewException extends IllegalArgumentException { + + public AlreadyReviewException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/review/application/exception/InvalidUserToReview.java b/backend/ddang/src/main/java/com/ddang/ddang/review/application/exception/InvalidUserToReview.java new file mode 100644 index 000000000..56990bae7 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/review/application/exception/InvalidUserToReview.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.review.application.exception; + +public class InvalidUserToReview extends IllegalArgumentException { + + public InvalidUserToReview(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/review/application/exception/ReviewNotFoundException.java b/backend/ddang/src/main/java/com/ddang/ddang/review/application/exception/ReviewNotFoundException.java new file mode 100644 index 000000000..aa7ae1ae7 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/review/application/exception/ReviewNotFoundException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.review.application.exception; + +public class ReviewNotFoundException extends IllegalArgumentException { + + public ReviewNotFoundException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/review/domain/Review.java b/backend/ddang/src/main/java/com/ddang/ddang/review/domain/Review.java new file mode 100644 index 000000000..65a7d41bf --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/review/domain/Review.java @@ -0,0 +1,67 @@ +package com.ddang.ddang.review.domain; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.common.entity.BaseCreateTimeEntity; +import com.ddang.ddang.user.domain.User; +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Entity +@NoArgsConstructor +@Getter +@EqualsAndHashCode(of = "id", callSuper = false) +@ToString(of = {"id", "content", "score"}) +public class Review extends BaseCreateTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "auction_id", nullable = false, foreignKey = @ForeignKey(name = "fk_review_auction")) + private Auction auction; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "writer_id", nullable = false, foreignKey = @ForeignKey(name = "fk_review_writer")) + private User writer; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "target_id", nullable = false, foreignKey = @ForeignKey(name = "fk_review_target")) + private User target; + + private String content; + + @Embedded + @AttributeOverride(name = "value", column = @Column(name = "score")) + private Score score; + + @Builder + private Review( + final Auction auction, + final User writer, + final User target, + final String content, + final Score score + ) { + this.auction = auction; + this.writer = writer; + this.target = target; + this.content = content; + this.score = score; + } +} + diff --git a/backend/ddang/src/main/java/com/ddang/ddang/review/domain/Score.java b/backend/ddang/src/main/java/com/ddang/ddang/review/domain/Score.java new file mode 100644 index 000000000..b6969892b --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/review/domain/Score.java @@ -0,0 +1,29 @@ +package com.ddang.ddang.review.domain; + +import com.ddang.ddang.review.domain.exception.InvalidScoreException; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@EqualsAndHashCode +@ToString +public class Score { + + private static final double SCORE_UNIT = 0.5; + + private double value; + + public Score(final double value) { + if (value % SCORE_UNIT != 0) { + throw new InvalidScoreException("평가 점수는 0.5 단위여야 합니다."); + } + + this.value = value; + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/review/domain/exception/InvalidScoreException.java b/backend/ddang/src/main/java/com/ddang/ddang/review/domain/exception/InvalidScoreException.java new file mode 100644 index 000000000..14fe9eb07 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/review/domain/exception/InvalidScoreException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.review.domain.exception; + +public class InvalidScoreException extends IllegalArgumentException { + + public InvalidScoreException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/review/infrastructure/persistence/JpaReviewRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/review/infrastructure/persistence/JpaReviewRepository.java new file mode 100644 index 000000000..0d6e71e6a --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/review/infrastructure/persistence/JpaReviewRepository.java @@ -0,0 +1,22 @@ +package com.ddang.ddang.review.infrastructure.persistence; + +import com.ddang.ddang.review.domain.Review; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.Optional; + +public interface JpaReviewRepository extends JpaRepository { + + boolean existsByAuctionIdAndWriterId(final Long auctionId, final Long writerId); + + @Query(""" + SELECT r FROM Review r JOIN FETCH r.writer w JOIN FETCH r.target t + WHERE t.id = :targetId + ORDER BY r.id DESC + """) + List findAllByTargetId(final Long targetId); + + Optional findByAuctionIdAndWriterId(final Long auctionId, final Long writerId); +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/review/presentation/ReviewController.java b/backend/ddang/src/main/java/com/ddang/ddang/review/presentation/ReviewController.java new file mode 100644 index 000000000..d8e5df0c5 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/review/presentation/ReviewController.java @@ -0,0 +1,60 @@ +package com.ddang.ddang.review.presentation; + +import com.ddang.ddang.authentication.configuration.AuthenticateUser; +import com.ddang.ddang.authentication.domain.dto.AuthenticationUserInfo; +import com.ddang.ddang.review.application.ReviewService; +import com.ddang.ddang.review.application.dto.CreateReviewDto; +import com.ddang.ddang.review.application.dto.ReadReviewDetailDto; +import com.ddang.ddang.review.application.dto.ReadReviewDto; +import com.ddang.ddang.review.presentation.dto.request.CreateReviewRequest; +import com.ddang.ddang.review.presentation.dto.response.ReadReviewDetailResponse; +import com.ddang.ddang.review.presentation.dto.response.ReadReviewResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.net.URI; +import java.util.List; + +@RestController +@RequestMapping("/reviews") +@RequiredArgsConstructor +public class ReviewController { + + private final ReviewService reviewService; + + @PostMapping + public ResponseEntity create( + @AuthenticateUser final AuthenticationUserInfo userInfo, + @RequestBody @Valid final CreateReviewRequest reviewRequest + ) { + final Long reviewId = reviewService.create(CreateReviewDto.of(userInfo.userId(), reviewRequest)); + + return ResponseEntity.created(URI.create("/reviews/" + reviewId)) + .build(); + } + + @GetMapping("/{reviewId}") + public ResponseEntity read(@PathVariable final Long reviewId) { + final ReadReviewDetailDto readReviewDetailDto = reviewService.readByReviewId(reviewId); + ReadReviewDetailResponse response = ReadReviewDetailResponse.from(readReviewDetailDto); + + return ResponseEntity.ok(response); + } + + @GetMapping("/users/{userId}") + public ResponseEntity> readAllReviewsOfTargetUser(@PathVariable final Long userId) { + final List readReviewDtos = reviewService.readAllByTargetId(userId); + final List response = readReviewDtos.stream() + .map(ReadReviewResponse::from) + .toList(); + + return ResponseEntity.ok(response); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/review/presentation/dto/request/CreateReviewRequest.java b/backend/ddang/src/main/java/com/ddang/ddang/review/presentation/dto/request/CreateReviewRequest.java new file mode 100644 index 000000000..00e240d26 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/review/presentation/dto/request/CreateReviewRequest.java @@ -0,0 +1,22 @@ +package com.ddang.ddang.review.presentation.dto.request; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; + +public record CreateReviewRequest( + + @NotNull(message = "경매 아이디가 입력되지 않았습니다.") + @Positive(message = "경매 아이디는 양수여야 합니다.") + Long auctionId, + + @NotNull(message = "평가할 상대방의 아이디가 입력되지 않았습니다.") + @Positive(message = "사용자 아이디는 양수여야 합니다.") + Long targetId, + + String content, + + @NotNull(message = "점수가 입력되지 않았습니다.") + @Positive(message = "점수는 양수여야 합니다.") + Float score +) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/review/presentation/dto/response/ReadReviewDetailResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/review/presentation/dto/response/ReadReviewDetailResponse.java new file mode 100644 index 000000000..93e2222a9 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/review/presentation/dto/response/ReadReviewDetailResponse.java @@ -0,0 +1,17 @@ +package com.ddang.ddang.review.presentation.dto.response; + +import com.ddang.ddang.review.application.dto.ReadReviewDetailDto; +import jakarta.annotation.Nullable; + + +public record ReadReviewDetailResponse(@Nullable Float score, @Nullable String content) { + + public static ReadReviewDetailResponse from(final ReadReviewDetailDto readReviewDetailDto) { + final Double nullableScore = readReviewDetailDto.score(); + if (nullableScore == null) { + return new ReadReviewDetailResponse(null, readReviewDetailDto.content()); + } + + return new ReadReviewDetailResponse(nullableScore.floatValue(), readReviewDetailDto.content()); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/review/presentation/dto/response/ReadReviewResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/review/presentation/dto/response/ReadReviewResponse.java new file mode 100644 index 000000000..07d033851 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/review/presentation/dto/response/ReadReviewResponse.java @@ -0,0 +1,36 @@ +package com.ddang.ddang.review.presentation.dto.response; + +import com.ddang.ddang.review.application.dto.ReadReviewDto; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDateTime; + +public record ReadReviewResponse( + Long id, + + ReadUserInReviewResponse writer, + + String content, + + Float score, + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul") + LocalDateTime createdTime +) { + + public static ReadReviewResponse from(final ReadReviewDto reviewDto) { + final Double nullableScore = reviewDto.score(); + Float returnScore = null; + if (nullableScore != null) { + returnScore = nullableScore.floatValue(); + } + + return new ReadReviewResponse( + reviewDto.id(), + ReadUserInReviewResponse.from(reviewDto.writer()), + reviewDto.content(), + returnScore, + reviewDto.createdTime() + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/review/presentation/dto/response/ReadUserInReviewResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/review/presentation/dto/response/ReadUserInReviewResponse.java new file mode 100644 index 000000000..328ed847c --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/review/presentation/dto/response/ReadUserInReviewResponse.java @@ -0,0 +1,16 @@ +package com.ddang.ddang.review.presentation.dto.response; + +import com.ddang.ddang.image.presentation.util.ImageRelativeUrl; +import com.ddang.ddang.image.presentation.util.ImageUrlCalculator; +import com.ddang.ddang.review.application.dto.ReadUserInReviewDto; + +public record ReadUserInReviewResponse(Long id, String name, String profileImage) { + + public static ReadUserInReviewResponse from(final ReadUserInReviewDto userDto) { + return new ReadUserInReviewResponse( + userDto.id(), + userDto.name(), + ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, userDto.profileImageId()) + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/application/UserService.java b/backend/ddang/src/main/java/com/ddang/ddang/user/application/UserService.java index d2a89764d..5dc1ca80c 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/user/application/UserService.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/application/UserService.java @@ -1,6 +1,9 @@ package com.ddang.ddang.user.application; +import com.ddang.ddang.image.domain.StoreImageProcessor; +import com.ddang.ddang.image.domain.dto.StoreImageDto; import com.ddang.ddang.user.application.dto.ReadUserDto; +import com.ddang.ddang.user.application.dto.UpdateUserDto; import com.ddang.ddang.user.application.exception.UserNotFoundException; import com.ddang.ddang.user.domain.User; import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; @@ -14,6 +17,7 @@ public class UserService { private final JpaUserRepository userRepository; + private final StoreImageProcessor imageProcessor; public ReadUserDto readById(final Long userId) { final User user = userRepository.findById(userId) @@ -23,10 +27,22 @@ public ReadUserDto readById(final Long userId) { } @Transactional - public void deleteById(final Long userId) { + public ReadUserDto updateById(final Long userId, final UpdateUserDto userDto) { final User user = userRepository.findByIdAndDeletedIsFalse(userId) .orElseThrow(() -> new UserNotFoundException("사용자 정보를 사용할 수 없습니다.")); - user.withdrawal(); + updateUserByRequest(userDto, user); + + return ReadUserDto.from(user); + } + + private void updateUserByRequest(final UpdateUserDto userDto, final User user) { + if (userDto.profileImage() != null) { + final StoreImageDto storeImageDto = imageProcessor.storeImageFile(userDto.profileImage()); + user.updateProfileImage(storeImageDto.toProfileImageEntity()); + } + if (userDto.name() != null) { + user.updateName(userDto.name()); + } } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/application/dto/ReadUserDto.java b/backend/ddang/src/main/java/com/ddang/ddang/user/application/dto/ReadUserDto.java index 9f0e44ff7..15ca854b6 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/user/application/dto/ReadUserDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/application/dto/ReadUserDto.java @@ -1,16 +1,25 @@ package com.ddang.ddang.user.application.dto; +import com.ddang.ddang.image.application.util.ImageIdProcessor; import com.ddang.ddang.user.domain.User; -public record ReadUserDto(Long id, String name, String profileImage, double reliability, String oauthId) { +public record ReadUserDto( + Long id, + String name, + Long profileImageId, + double reliability, + String oauthId, + boolean isDeleted +) { public static ReadUserDto from(final User user) { return new ReadUserDto( user.getId(), user.getName(), - user.getProfileImage(), - user.getReliability(), - user.getOauthId() + ImageIdProcessor.process(user.getProfileImage()), + user.getReliability().getValue(), + user.getOauthId(), + user.isDeleted() ); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/application/dto/UpdateUserDto.java b/backend/ddang/src/main/java/com/ddang/ddang/user/application/dto/UpdateUserDto.java new file mode 100644 index 000000000..1e34ce9c3 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/application/dto/UpdateUserDto.java @@ -0,0 +1,11 @@ +package com.ddang.ddang.user.application.dto; + +import com.ddang.ddang.user.presentation.dto.request.UpdateUserRequest; +import org.springframework.web.multipart.MultipartFile; + +public record UpdateUserDto(String name, MultipartFile profileImage) { + + public static UpdateUserDto of(final UpdateUserRequest userRequest, final MultipartFile profileImage) { + return new UpdateUserDto(userRequest.name(), profileImage); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/domain/Reliability.java b/backend/ddang/src/main/java/com/ddang/ddang/user/domain/Reliability.java new file mode 100644 index 000000000..cda3996cf --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/domain/Reliability.java @@ -0,0 +1,40 @@ +package com.ddang.ddang.user.domain; + +import com.ddang.ddang.review.domain.Review; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.List; + +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@EqualsAndHashCode +@ToString +public class Reliability { + + public static final Reliability INITIAL_RELIABILITY = new Reliability(null); + + private Double value; + + public Reliability(final Double value) { + this.value = value; + } + + public void updateReliability(final List reviews) { + if (reviews.isEmpty()) { + this.value = null; + + return; + } + + this.value = reviews.stream() + .mapToDouble(review -> review.getScore().getValue()) + .average() + .orElseGet(null); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/domain/User.java b/backend/ddang/src/main/java/com/ddang/ddang/user/domain/User.java index 107602dd4..b6bcf6278 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/user/domain/User.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/domain/User.java @@ -1,11 +1,20 @@ package com.ddang.ddang.user.domain; import com.ddang.ddang.common.entity.BaseTimeEntity; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.review.domain.Review; +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; +import jakarta.persistence.Embedded; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import lombok.AccessLevel; import lombok.Builder; @@ -14,15 +23,18 @@ import lombok.NoArgsConstructor; import lombok.ToString; +import java.util.List; + @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter -@EqualsAndHashCode(of = "id") -@ToString +@EqualsAndHashCode(of = "id", callSuper = false) +@ToString(of = {"id", "name", "reliability", "oauthId", "deleted"}) @Table(name = "users") public class User extends BaseTimeEntity { private static final boolean DELETED_STATUS = true; + private static final String UNKOWN_NAME = "알 수 없음"; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -31,11 +43,14 @@ public class User extends BaseTimeEntity { @Column(unique = true) private String name; - private String profileImage; + @OneToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.REMOVE}) + @JoinColumn(name = "profile_image_id", foreignKey = @ForeignKey(name = "fk_user_profile_image"), nullable = false) + private ProfileImage profileImage; - private double reliability; + @Embedded + @AttributeOverride(name = "value", column = @Column(name = "reliability")) + private Reliability reliability; - @Column(unique = true) private String oauthId; @Column(name = "is_deleted") @@ -44,17 +59,37 @@ public class User extends BaseTimeEntity { @Builder private User( final String name, - final String profileImage, - final double reliability, + final ProfileImage profileImage, + final Reliability reliability, final String oauthId ) { this.name = name; this.profileImage = profileImage; - this.reliability = reliability; + this.reliability = processReliability(reliability); this.oauthId = oauthId; } + private Reliability processReliability(final Reliability reliability) { + if (reliability == null) { + return Reliability.INITIAL_RELIABILITY; + } + + return reliability; + } + + public void updateName(final String name) { + this.name = name; + } + + public void updateProfileImage(final ProfileImage profileImage) { + this.profileImage = profileImage; + } + public void withdrawal() { this.deleted = DELETED_STATUS; } + + public void updateReliability(final List reviews) { + reliability.updateReliability(reviews); + } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/infrastructure/persistence/JpaUserRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/user/infrastructure/persistence/JpaUserRepository.java index 44bda95bb..390768b88 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/user/infrastructure/persistence/JpaUserRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/infrastructure/persistence/JpaUserRepository.java @@ -7,9 +7,11 @@ public interface JpaUserRepository extends JpaRepository { - Optional findByOauthId(final String oauthId); + Optional findByOauthIdAndDeletedIsFalse(final String oauthId); Optional findByIdAndDeletedIsFalse(final Long id); boolean existsByIdAndDeletedIsTrue(final Long id); + + boolean existsByNameEndingWith(final String name); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/UserAuctionController.java b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/UserAuctionController.java new file mode 100644 index 000000000..29c6ca241 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/UserAuctionController.java @@ -0,0 +1,44 @@ +package com.ddang.ddang.user.presentation; + +import com.ddang.ddang.auction.application.AuctionService; +import com.ddang.ddang.auction.application.dto.ReadAuctionsDto; +import com.ddang.ddang.auction.configuration.DescendingSort; +import com.ddang.ddang.authentication.configuration.AuthenticateUser; +import com.ddang.ddang.authentication.domain.dto.AuthenticationUserInfo; +import com.ddang.ddang.user.presentation.dto.response.ReadAuctionsResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/users/auctions") +@RequiredArgsConstructor +public class UserAuctionController { + + private final AuctionService auctionService; + + @GetMapping("/mine") + public ResponseEntity readAllByUserInfo( + @AuthenticateUser final AuthenticationUserInfo userInfo, + @DescendingSort final Pageable pageable + ) { + final ReadAuctionsDto readAuctionsDto = auctionService.readAllByUserId(userInfo.userId(), pageable); + final ReadAuctionsResponse response = ReadAuctionsResponse.from(readAuctionsDto); + + return ResponseEntity.ok(response); + } + + @GetMapping("/bids") + public ResponseEntity readAllByBids( + @AuthenticateUser final AuthenticationUserInfo userInfo, + @DescendingSort final Pageable pageable + ) { + final ReadAuctionsDto readAuctionsDto = auctionService.readAllByBidderId(userInfo.userId(), pageable); + final ReadAuctionsResponse response = ReadAuctionsResponse.from(readAuctionsDto); + + return ResponseEntity.ok(response); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/UserController.java b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/UserController.java index 431e1690d..a47bcdf31 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/UserController.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/UserController.java @@ -4,13 +4,18 @@ import com.ddang.ddang.authentication.domain.dto.AuthenticationUserInfo; import com.ddang.ddang.user.application.UserService; import com.ddang.ddang.user.application.dto.ReadUserDto; -import com.ddang.ddang.user.presentation.dto.ReadUserResponse; +import com.ddang.ddang.user.presentation.dto.response.ReadUserResponse; +import com.ddang.ddang.user.application.dto.UpdateUserDto; +import com.ddang.ddang.user.presentation.dto.request.UpdateUserRequest; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/users") @@ -27,11 +32,20 @@ public ResponseEntity readById(@AuthenticateUser final Authent return ResponseEntity.ok(response); } - @DeleteMapping("/withdrawal") - public ResponseEntity delete(@AuthenticateUser final AuthenticationUserInfo userInfo) { - userService.deleteById(userInfo.userId()); + @PatchMapping + public ResponseEntity updateById( + @AuthenticateUser final AuthenticationUserInfo userInfo, + @RequestPart(required = false) @Valid final UpdateUserRequest request, + @RequestPart(required = false) final MultipartFile profileImage + ) { + UpdateUserDto updateUserDto = null; + if (request != null) { + updateUserDto = UpdateUserDto.of(request, profileImage); + } - return ResponseEntity.noContent() - .build(); + final ReadUserDto readUserDto = userService.updateById(userInfo.userId(), updateUserDto); + final ReadUserResponse response = ReadUserResponse.from(readUserDto); + + return ResponseEntity.ok(response); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/ReadUserResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/ReadUserResponse.java index 84b8340d7..ccc8563ff 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/ReadUserResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/ReadUserResponse.java @@ -1,10 +1,18 @@ package com.ddang.ddang.user.presentation.dto; +import com.ddang.ddang.image.presentation.util.ImageRelativeUrl; +import com.ddang.ddang.image.presentation.util.ImageUrlCalculator; import com.ddang.ddang.user.application.dto.ReadUserDto; +import com.ddang.ddang.user.presentation.util.NameProcessor; -public record ReadUserResponse(String name, String profileImage, double reliability) { +public record ReadUserResponse(String name, String profileImage, Float reliability) { public static ReadUserResponse from(final ReadUserDto readUserDto) { - return new ReadUserResponse(readUserDto.name(), readUserDto.profileImage(), readUserDto.reliability()); + final String name = NameProcessor.process(readUserDto.isDeleted(), readUserDto.name()); + final String profileImageUrl = ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, readUserDto.profileImageId()); + + final Float floatReliability = Float.valueOf(String.valueOf(readUserDto.reliability())); + + return new ReadUserResponse(name, profileImageUrl, floatReliability); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/request/UpdateUserRequest.java b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/request/UpdateUserRequest.java new file mode 100644 index 000000000..381fed949 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/request/UpdateUserRequest.java @@ -0,0 +1,6 @@ +package com.ddang.ddang.user.presentation.dto.request; + +import jakarta.validation.constraints.NotEmpty; + +public record UpdateUserRequest(@NotEmpty(message = "수정하려는 이름이 입력되지 않았습니다.") String name) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/AuctionDetailResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/AuctionDetailResponse.java new file mode 100644 index 000000000..d7fa2befd --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/AuctionDetailResponse.java @@ -0,0 +1,71 @@ +package com.ddang.ddang.user.presentation.dto.response; + +import com.ddang.ddang.auction.application.dto.ReadAuctionDto; +import com.ddang.ddang.image.presentation.util.ImageRelativeUrl; +import com.ddang.ddang.image.presentation.util.ImageUrlCalculator; +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.LocalDateTime; +import java.util.List; + +public record AuctionDetailResponse( + Long id, + + List images, + + String title, + + CategoryResponse category, + + String description, + + int startPrice, + + Integer lastBidPrice, + + String status, + + int bidUnit, + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul") + LocalDateTime registerTime, + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul") + LocalDateTime closingTime, + + List directRegions, + + int auctioneerCount +) { + + public static AuctionDetailResponse from(final ReadAuctionDto dto) { + return new AuctionDetailResponse( + dto.id(), + convertImageFullUrls(dto), + dto.title(), + new CategoryResponse(dto.mainCategory(), dto.subCategory()), + dto.description(), + dto.startPrice(), + dto.lastBidPrice(), + dto.auctionStatus().name(), + dto.bidUnit(), + dto.registerTime(), + dto.closingTime(), + convertDirectRegionsResponse(dto), + dto.auctioneerCount() + ); + } + + private static List convertImageFullUrls(final ReadAuctionDto dto) { + return dto.auctionImageIds() + .stream() + .map(id -> ImageUrlCalculator.calculateBy(ImageRelativeUrl.AUCTION, id)) + .toList(); + } + + private static List convertDirectRegionsResponse(final ReadAuctionDto dto) { + return dto.auctionRegions() + .stream() + .map(DirectRegionResponse::from) + .toList(); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/CategoryResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/CategoryResponse.java new file mode 100644 index 000000000..3ef1a5b86 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/CategoryResponse.java @@ -0,0 +1,4 @@ +package com.ddang.ddang.user.presentation.dto.response; + +public record CategoryResponse(String main, String sub) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ChatRoomInAuctionResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ChatRoomInAuctionResponse.java new file mode 100644 index 000000000..b77c250d9 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ChatRoomInAuctionResponse.java @@ -0,0 +1,11 @@ +package com.ddang.ddang.user.presentation.dto.response; + +import com.ddang.ddang.auction.application.dto.ReadChatRoomDto; + +public record ChatRoomInAuctionResponse(Long id, boolean isChatParticipant) { + + public static ChatRoomInAuctionResponse from(final ReadChatRoomDto readChatRoomDto) { + + return new ChatRoomInAuctionResponse(readChatRoomDto.id(), readChatRoomDto.isChatParticipant()); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/DirectRegionResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/DirectRegionResponse.java new file mode 100644 index 000000000..5295a3c77 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/DirectRegionResponse.java @@ -0,0 +1,14 @@ +package com.ddang.ddang.user.presentation.dto.response; + +import com.ddang.ddang.auction.application.dto.ReadRegionsDto; + +public record DirectRegionResponse(String first, String second, String third) { + + public static DirectRegionResponse from(final ReadRegionsDto dto) { + return new DirectRegionResponse( + dto.firstRegionDto().regionName(), + dto.secondRegionDto().regionName(), + dto.thirdRegionDto().regionName() + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadAuctionDetailResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadAuctionDetailResponse.java new file mode 100644 index 000000000..2a5e68862 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadAuctionDetailResponse.java @@ -0,0 +1,44 @@ +package com.ddang.ddang.user.presentation.dto.response; + +import com.ddang.ddang.auction.application.dto.ReadAuctionWithChatRoomIdDto; +import com.ddang.ddang.authentication.domain.dto.AuthenticationUserInfo; +import com.ddang.ddang.image.presentation.util.ImageRelativeUrl; +import com.ddang.ddang.image.presentation.util.ImageUrlCalculator; + +public record ReadAuctionDetailResponse( + AuctionDetailResponse auction, + SellerResponse seller, + ChatRoomInAuctionResponse chat, + boolean isOwner +) { + + public static ReadAuctionDetailResponse of( + final ReadAuctionWithChatRoomIdDto dto, + final AuthenticationUserInfo userInfo + ) { + final AuctionDetailResponse auctionDetailResponse = AuctionDetailResponse.from(dto.auctionDto()); + final String profileImageUrl = ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, dto.auctionDto().sellerId()); + + final Float floatReliability = Float.valueOf(String.valueOf(dto.auctionDto().sellerReliability())); + + final SellerResponse sellerResponse = new SellerResponse( + dto.auctionDto().sellerId(), + profileImageUrl, + dto.auctionDto().sellerName(), + floatReliability + ); + + final ChatRoomInAuctionResponse chatRoomResponse = ChatRoomInAuctionResponse.from(dto.chatRoomDto()); + + return new ReadAuctionDetailResponse( + auctionDetailResponse, + sellerResponse, + chatRoomResponse, + isOwner(dto, userInfo) + ); + } + + private static boolean isOwner(final ReadAuctionWithChatRoomIdDto dto, final AuthenticationUserInfo userInfo) { + return dto.auctionDto().sellerId().equals(userInfo.userId()); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadAuctionResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadAuctionResponse.java new file mode 100644 index 000000000..4db39f7f5 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadAuctionResponse.java @@ -0,0 +1,40 @@ +package com.ddang.ddang.user.presentation.dto.response; + +import com.ddang.ddang.auction.application.dto.ReadAuctionDto; +import com.ddang.ddang.image.presentation.util.ImageRelativeUrl; +import com.ddang.ddang.image.presentation.util.ImageUrlCalculator; + +public record ReadAuctionResponse( + Long id, + String title, + String image, + int auctionPrice, + String status, + int auctioneerCount +) { + + public static ReadAuctionResponse from(final ReadAuctionDto dto) { + return new ReadAuctionResponse( + dto.id(), + dto.title(), + calculateThumbnailImageUrl(dto), + processAuctionPrice(dto.startPrice(), dto.lastBidPrice()), + dto.auctionStatus().name(), + dto.auctioneerCount() + ); + } + + private static String calculateThumbnailImageUrl(final ReadAuctionDto dto) { + final Long thumbnailAuctionImage = dto.auctionImageIds().get(0); + + return ImageUrlCalculator.calculateBy(ImageRelativeUrl.AUCTION, thumbnailAuctionImage); + } + + private static int processAuctionPrice(final Integer startPrice, final Integer lastBidPrice) { + if (lastBidPrice == null) { + return startPrice; + } + + return lastBidPrice; + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadAuctionWithChatRoomIdDto.java b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadAuctionWithChatRoomIdDto.java new file mode 100644 index 000000000..8aaf07b15 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadAuctionWithChatRoomIdDto.java @@ -0,0 +1,7 @@ +package com.ddang.ddang.user.presentation.dto.response; + +import com.ddang.ddang.auction.application.dto.ReadAuctionDto; +import com.ddang.ddang.auction.application.dto.ReadChatRoomDto; + +public record ReadAuctionWithChatRoomIdDto(ReadAuctionDto auctionDto, ReadChatRoomDto chatRoomDto) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadAuctionsResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadAuctionsResponse.java new file mode 100644 index 000000000..2989b6849 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadAuctionsResponse.java @@ -0,0 +1,17 @@ +package com.ddang.ddang.user.presentation.dto.response; + +import com.ddang.ddang.auction.application.dto.ReadAuctionsDto; + +import java.util.List; + +public record ReadAuctionsResponse(List auctions, boolean isLast) { + + public static ReadAuctionsResponse from(final ReadAuctionsDto readAuctionsDto) { + final List readAuctionResponses = readAuctionsDto.readAuctionDtos() + .stream() + .map(ReadAuctionResponse::from) + .toList(); + + return new ReadAuctionsResponse(readAuctionResponses, readAuctionsDto.isLast()); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadUserResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadUserResponse.java new file mode 100644 index 000000000..0c83b2e5a --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadUserResponse.java @@ -0,0 +1,19 @@ +package com.ddang.ddang.user.presentation.dto.response; + +import com.ddang.ddang.image.presentation.util.ImageRelativeUrl; +import com.ddang.ddang.image.presentation.util.ImageUrlCalculator; +import com.ddang.ddang.user.application.dto.ReadUserDto; +import com.ddang.ddang.user.presentation.util.NameProcessor; + +public record ReadUserResponse(String name, String profileImage, Float reliability) { + + public static ReadUserResponse from(final ReadUserDto readUserDto) { + final Float floatReliability = Float.valueOf(String.valueOf(readUserDto.reliability())); + + return new ReadUserResponse( + NameProcessor.process(readUserDto.isDeleted(), readUserDto.name()), + ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, readUserDto.profileImageId()), + floatReliability + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/SellerResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/SellerResponse.java new file mode 100644 index 000000000..219199ce4 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/SellerResponse.java @@ -0,0 +1,9 @@ +package com.ddang.ddang.user.presentation.dto.response; + +public record SellerResponse( + Long id, + String image, + String nickname, + Float reliability +) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/util/NameProcessor.java b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/util/NameProcessor.java new file mode 100644 index 000000000..2e82a0949 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/util/NameProcessor.java @@ -0,0 +1,17 @@ +package com.ddang.ddang.user.presentation.util; + +public class NameProcessor { + + private static final String UNKNOWN_NAME = "알 수 없음"; + + private NameProcessor() { + } + + public static String process(final boolean isDeleted, final String name) { + if (isDeleted) { + return UNKNOWN_NAME; + } + + return name; + } +} diff --git a/backend/ddang/src/main/resources/application-local.yml b/backend/ddang/src/main/resources/application-local.yml index 3224d4c33..2db9e094b 100644 --- a/backend/ddang/src/main/resources/application-local.yml +++ b/backend/ddang/src/main/resources/application-local.yml @@ -49,4 +49,11 @@ oauth2: client: providers: kakao: + admin-key: adminkey user-info-uri: https://kapi.kakao.com/v2/user/me + user-unlink-uri: https://kapi.kakao.com/v1/user/unlink + +fcm: + enabled: false + key: + path: firebase/private-key.json diff --git a/backend/ddang/src/main/resources/application.yml b/backend/ddang/src/main/resources/application.yml index 628577275..6a010889f 100644 --- a/backend/ddang/src/main/resources/application.yml +++ b/backend/ddang/src/main/resources/application.yml @@ -9,7 +9,9 @@ spring: - file-warn-logging - file-error-logging - slack-error-logging + - file-info-request-logging prod: - file-warn-logging - file-error-logging - slack-error-logging + - file-info-request-logging diff --git a/backend/ddang/src/main/resources/db/migration/V10__create_device_token_tables.sql b/backend/ddang/src/main/resources/db/migration/V10__create_device_token_tables.sql new file mode 100644 index 000000000..83b440523 --- /dev/null +++ b/backend/ddang/src/main/resources/db/migration/V10__create_device_token_tables.sql @@ -0,0 +1,8 @@ +create table device_token ( + id bigint not null auto_increment, + device_token varchar(255) not null, + user_id bigint, + primary key (id) +); + +alter table device_token add constraint fk_device_token_user foreign key (user_id) references users (id); diff --git a/backend/ddang/src/main/resources/db/migration/V11__create_profile_image_tables.sql b/backend/ddang/src/main/resources/db/migration/V11__create_profile_image_tables.sql new file mode 100644 index 000000000..8965cc9cd --- /dev/null +++ b/backend/ddang/src/main/resources/db/migration/V11__create_profile_image_tables.sql @@ -0,0 +1,10 @@ +create table profile_image ( + id bigint not null auto_increment, + store_name varchar(255), + upload_name varchar(255), + primary key (id) +); + +alter table users drop profile_image; +alter table users add profile_image_id bigint; +alter table users add constraint fk_user_profile_image foreign key (profile_image_id) references profile_image (id); diff --git a/backend/ddang/src/main/resources/db/migration/V12__alter_user_tables.sql b/backend/ddang/src/main/resources/db/migration/V12__alter_user_tables.sql new file mode 100644 index 000000000..ab1a30431 --- /dev/null +++ b/backend/ddang/src/main/resources/db/migration/V12__alter_user_tables.sql @@ -0,0 +1 @@ +ALTER TABLE users DROP INDEX uq_oauth_id; diff --git a/backend/ddang/src/main/resources/db/migration/V13__alter_user_tables.sql b/backend/ddang/src/main/resources/db/migration/V13__alter_user_tables.sql new file mode 100644 index 000000000..a4d931ec2 --- /dev/null +++ b/backend/ddang/src/main/resources/db/migration/V13__alter_user_tables.sql @@ -0,0 +1,2 @@ +UPDATE users SET profile_image_id = 1 WHERE profile_image_id is null; +ALTER TABLE users MODIFY profile_image_id bigint NOT NULL; diff --git a/backend/ddang/src/main/resources/db/migration/V14__alter_chat_room_report_tables.sql b/backend/ddang/src/main/resources/db/migration/V14__alter_chat_room_report_tables.sql new file mode 100644 index 000000000..99b16f353 --- /dev/null +++ b/backend/ddang/src/main/resources/db/migration/V14__alter_chat_room_report_tables.sql @@ -0,0 +1,3 @@ +alter table chat_room_report drop foreign key fk_chat_room_report_auction; +alter table chat_room_report drop index fk_chat_room_report_auction; +alter table chat_room_report add constraint fk_chat_room_report_chat_room foreign key (chat_room_id) references chat_room (id); diff --git a/backend/ddang/src/main/resources/db/migration/V15__create_review_tables.sql b/backend/ddang/src/main/resources/db/migration/V15__create_review_tables.sql new file mode 100644 index 000000000..b0f97cfed --- /dev/null +++ b/backend/ddang/src/main/resources/db/migration/V15__create_review_tables.sql @@ -0,0 +1,14 @@ +create table review ( + id bigint not null auto_increment, + created_time datetime(6) not null, + content varchar(255), + score float not null, + auction_id bigint not null, + writer_id bigint not null, + target_id bigint not null, + primary key (id) +); + +alter table review add constraint fk_review_auction foreign key (auction_id) references auction (id); +alter table review add constraint fk_review_writer foreign key (writer_id) references users (id); +alter table review add constraint fk_review_target foreign key (target_id) references users (id); diff --git a/backend/ddang/src/main/resources/db/migration/V16__create_question_and_answer_tables.sql b/backend/ddang/src/main/resources/db/migration/V16__create_question_and_answer_tables.sql new file mode 100644 index 000000000..41ba275c9 --- /dev/null +++ b/backend/ddang/src/main/resources/db/migration/V16__create_question_and_answer_tables.sql @@ -0,0 +1,47 @@ +create table question +( + id bigint not null auto_increment, + created_time datetime(6) not null, + content text, + is_deleted bit, + auction_id bigint, + writer_id bigint, + primary key (id) +); +create table answer +( + id bigint not null auto_increment, + created_time datetime(6) not null, + content text, + is_deleted bit, + question_id bigint, + primary key (id) +); + +create table question_report +( + id bigint not null auto_increment, + created_time datetime(6) not null, + description text, + question_id bigint, + reporter_id bigint, + primary key (id) +); +create table answer_report +( + id bigint not null auto_increment, + created_time datetime(6) not null, + description text, + answer_id bigint, + reporter_id bigint, + primary key (id) +); + +alter table question add constraint fk_question_auction foreign key (auction_id) references auction (id); +alter table question add constraint fk_question_writer foreign key (writer_id) references users (id); +alter table answer add constraint fk_answer_question foreign key (question_id) references question (id); + +alter table question_report add constraint fk_question_report_question foreign key (question_id) references question (id); +alter table question_report add constraint fk_question_report_reporter foreign key (reporter_id) references users (id); +alter table answer_report add constraint fk_answer_report_answer foreign key (answer_id) references answer (id); +alter table answer_report add constraint fk_answer_report_reporter foreign key (reporter_id) references users (id); diff --git a/backend/ddang/src/main/resources/log/console-logging.xml b/backend/ddang/src/main/resources/log/console-logging.xml new file mode 100644 index 000000000..8a4bc9928 --- /dev/null +++ b/backend/ddang/src/main/resources/log/console-logging.xml @@ -0,0 +1,16 @@ + + + + INFO + ACCEPT + DENY + + + + + + + + + + diff --git a/backend/ddang/src/main/resources/log/file-error-logging.xml b/backend/ddang/src/main/resources/log/file-error-logging.xml new file mode 100644 index 000000000..11661c461 --- /dev/null +++ b/backend/ddang/src/main/resources/log/file-error-logging.xml @@ -0,0 +1,42 @@ + + + + ${LOG_DIR}/error/error-${BY_DATE}.log + + + ERROR + + + + + + timeStamp + yyyy-MM-dd HH:mm:ss + + + + + { + "message": "%message", + "stacktrace": "%rEx" + } + + + + utf-8 + + + + ${LOG_DIR}/backup/error/error-%d{yyyy-MM-dd}.%i.log + + 100MB + 30 + 3GB + + + + + + + + diff --git a/backend/ddang/src/main/resources/log/file-info-request-logging.xml b/backend/ddang/src/main/resources/log/file-info-request-logging.xml new file mode 100644 index 000000000..2ddd405d1 --- /dev/null +++ b/backend/ddang/src/main/resources/log/file-info-request-logging.xml @@ -0,0 +1,36 @@ + + + + ${LOG_DIR}/request-trace/trace-${BY_DATE}.log + + + INFO + ACCEPT + DENY + + + + + + timeStamp + yyyy-MM-dd HH:mm:ss + + + + + utf8 + + + + ${LOG_DIR}/backup/request-trace/trace-%d{yyyy-MM-dd}.%i.log + 100MB + 30 + 3GB + + + + + + + + diff --git a/backend/ddang/src/main/resources/log/file-warn-logging.xml b/backend/ddang/src/main/resources/log/file-warn-logging.xml new file mode 100644 index 000000000..4f2843e0e --- /dev/null +++ b/backend/ddang/src/main/resources/log/file-warn-logging.xml @@ -0,0 +1,36 @@ + + + + ${LOG_DIR}/warn/warn-${BY_DATE}.log + + + WARN + ACCEPT + DENY + + + + + + timeStamp + yyyy-MM-dd HH:mm:ss + + + + + utf-8 + + + + ${LOG_DIR}/backup/warn/warn-%d{yyyy-MM-dd}.%i.log + 100MB + 30 + 3GB + + + + + + + + diff --git a/backend/ddang/src/main/resources/log/slack-error-logging.xml b/backend/ddang/src/main/resources/log/slack-error-logging.xml new file mode 100644 index 000000000..df71fb0d8 --- /dev/null +++ b/backend/ddang/src/main/resources/log/slack-error-logging.xml @@ -0,0 +1,17 @@ + + + + ERROR + + + + + + + + + + + + + diff --git a/backend/ddang/src/main/resources/logback-spring.xml b/backend/ddang/src/main/resources/logback-spring.xml index fc568c6ed..5f91852a9 100644 --- a/backend/ddang/src/main/resources/logback-spring.xml +++ b/backend/ddang/src/main/resources/logback-spring.xml @@ -1,82 +1,13 @@ - - - - - - - - - - - - - - - ${LOG_DIR}/warn/warn-${BY_DATE}.log - - WARN - - - ${LOG_PATTERN} - utf8 - - - ${LOG_DIR}/backup/warn/warn-%d{yyyy-MM-dd}.%i.log - 100MB - 30 - 3GB - - - - - - - - - - - ${LOG_DIR}/error/error-${BY_DATE}.log - - ERROR - ACCEPT - DENY - - - ${LOG_PATTERN} - utf8 - - - ${LOG_DIR}/backup/error/error-%d{yyyy-MM-dd}.%i.log - - 100MB - 30 - 3GB - - - - - - - - - - - - - - ERROR - - - + + + + + - - - - diff --git a/backend/ddang/src/main/resources/static/docs/docs.html b/backend/ddang/src/main/resources/static/docs/docs.html index 7b813699a..ecfe045b1 100644 --- a/backend/ddang/src/main/resources/static/docs/docs.html +++ b/backend/ddang/src/main/resources/static/docs/docs.html @@ -453,6 +453,13 @@

땅땅땅 API 문서

  • AccessToken 재발급
  • AccessToken 유효성 검사
  • 로그아웃
  • +
  • 탈퇴
  • + + +
  • 사용자 정보 API +
  • 카테고리 API @@ -472,10 +479,21 @@

    땅땅땅 API 문서

  • +
  • Q&A API + +
  • 입찰 API +
  • +
  • 디바이스 토큰 API + +
  • +
  • 유저 상호 평가 API +
  • @@ -559,7 +594,8 @@

    요청

    Content-Type: application/json { - "accessToken" : "kakaoAccessToken" + "accessToken" : "kakaoAccessToken", + "deviceToken" : "deviceToken" } @@ -601,6 +637,11 @@

    요청

    String

    소셜 로그인 AccessToken

    + +

    deviceToken

    +

    String

    +

    기기 디바이스 토큰

    + @@ -612,8 +653,8 @@

    응답

    Content-Type: application/json { - "accessToken" : "accessToken", - "refreshToken" : "refreshToken" + "accessToken" : "Bearer accessToken", + "refreshToken" : "Bearer refreshToken" } @@ -655,7 +696,7 @@

    요청

    Content-Type: application/json { - "refreshToken" : "refreshToken" + "refreshToken" : "Bearer refreshToken" } @@ -689,8 +730,8 @@

    응답

    Content-Type: application/json { - "accessToken" : "accessToken", - "refreshToken" : "refreshToken" + "accessToken" : "Bearer accessToken", + "refreshToken" : "Bearer refreshToken" } @@ -851,6 +892,211 @@

    응답

    +
    +

    탈퇴

    +
    +

    요청

    +
    +
    +
    POST /oauth2/withdrawal/kakao HTTP/1.1
    +Content-Type: application/json
    +Authorization: Bearer accessToken
    +
    +{
    +  "refreshToken" : "Bearer refreshToken"
    +}
    +
    +
    + + ++++ + + + + + + + + + + + + +
    Table 2. /oauth2/withdrawal/{oauth2Type}
    ParameterDescription

    oauth2Type

    소셜 로그인을 할 서비스 선택(kakao로 고정)

    + ++++ + + + + + + + + + + + + +
    NameDescription

    Authorization

    회원 Bearer 인증 정보

    + +++++ + + + + + + + + + + + + + + +
    PathTypeDescription

    refreshToken

    String

    refreshToken

    +
    +
    +

    응답

    +
    +
    +
    HTTP/1.1 204 No Content
    +
    +
    +
    +
    + + +
    +

    사용자 정보 API

    +
    +
    +

    사용자 정보 조회

    +
    +

    요청

    +
    +
    +
    GET /users HTTP/1.1
    +Authorization: Bearer accessToken
    +
    +
    +
    +
    +

    응답

    +
    +
    +
    HTTP/1.1 200 OK
    +Content-Type: application/json
    +
    +{
    +  "name" : "사용자1",
    +  "profileImage" : "http://localhost:8080/users/images/1",
    +  "reliability" : 4.6
    +}
    +
    +
    + +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
    PathTypeDescription

    name

    String

    사용자 닉네임

    profileImage

    String

    사용자 프로필 이미지

    reliability

    Number

    사용자 신뢰도

    +
    +
    +
    +

    사용자 정보 수정

    +
    +

    요청

    +
    +
    +
    PATCH /users HTTP/1.1
    +Content-Type: multipart/form-data; boundary=6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm
    +Authorization: Bearer accessToken
    +
    +
    +
    +
    +

    응답

    +
    +
    +
    HTTP/1.1 200 OK
    +Content-Type: application/json
    +
    +{
    +  "name" : "updateName",
    +  "profileImage" : "http://localhost:8080/users/images/1",
    +  "reliability" : 4.6
    +}
    +
    +
    + +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
    PathTypeDescription

    name

    String

    사용자 닉네임

    profileImage

    String

    사용자 프로필 이미지

    reliability

    Number

    사용자 신뢰도

    +
    +
    @@ -859,7 +1105,7 @@

    카테고

    메인 카테고리 조회

    -

    요청

    +

    요청

    GET /categories HTTP/1.1
    @@ -868,7 +1114,7 @@ 

    요청

    -

    응답

    +

    응답

    HTTP/1.1 200 OK
    @@ -914,7 +1160,7 @@ 

    응답

    서브 카테고리 조회

    -

    요청

    +

    요청

    GET /categories/1 HTTP/1.1
    @@ -923,7 +1169,7 @@ 

    요청

    -

    응답

    +

    응답

    HTTP/1.1 200 OK
    @@ -974,7 +1220,7 @@ 

    첫 번째 직거래 지역 조회

    -

    요청

    +

    요청

    GET /regions HTTP/1.1
    @@ -983,7 +1229,7 @@ 

    요청

    -

    응답

    +

    응답

    HTTP/1.1 200 OK
    @@ -1034,7 +1280,7 @@ 

    응답

    두 번째 직거래 지역 조회

    -

    요청

    +

    요청

    GET /regions/1 HTTP/1.1
    @@ -1042,7 +1288,7 @@ 

    요청

    - +@@ -1062,17 +1308,17 @@

    요청

    Table 2. /regions/{firstId}Table 3. /regions/{firstId}
    -

    응답

    +

    응답

    HTTP/1.1 200 OK
     Content-Type: application/json
     
     [ {
    -  "id" : 2,
    +  "id" : 4,
       "name" : "강남구"
     }, {
    -  "id" : 3,
    +  "id" : 5,
       "name" : "강동구"
     } ]
    @@ -1113,15 +1359,15 @@

    응답

    세 번째 직거래 지역 조회

    -

    요청

    +

    요청

    -
    GET /regions/1/2 HTTP/1.1
    +
    GET /regions/1/4 HTTP/1.1
     Content-Type: application/json
    - +@@ -1145,17 +1391,17 @@

    요청

    Table 3. /regions/{firstId}/{secondId}Table 4. /regions/{firstId}/{secondId}
    -

    응답

    +

    응답

    HTTP/1.1 200 OK
     Content-Type: application/json
     
     [ {
    -  "id" : 3,
    +  "id" : 7,
       "name" : "개포1동"
     }, {
    -  "id" : 4,
    +  "id" : 8,
       "name" : "개포2동"
     } ]
    @@ -1201,7 +1447,7 @@

    경매 API

    경매 등록

    -

    요청

    +

    요청

    POST /auctions HTTP/1.1
    @@ -1217,7 +1463,7 @@ 

    요청

    Content-Disposition: form-data; name=request; filename=request Content-Type: application/json -{"title":"경매 상품 1","description":"이것은 경매 상품 1 입니다.","bidUnit":1000,"startPrice":1000,"closingTime":"2023-08-19T16:30:53.364176","subCategoryId":2,"thirdRegionIds":[3]} +{"title":"제목","description":"내용","bidUnit":1000,"startPrice":1000,"closingTime":"2023-10-12T11:02:42.5505268","subCategoryId":2,"thirdRegionIds":[3]} --6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm--
    @@ -1263,7 +1509,7 @@

    요청

    -

    응답

    +

    응답

    HTTP/1.1 201 Created
    @@ -1272,7 +1518,7 @@ 

    응답

    { "id" : 1, - "title" : "title", + "title" : "제목", "image" : "http://localhost:8080/auctions/images/1", "auctionPrice" : 1000, "status" : "UNBIDDEN", @@ -1331,11 +1577,12 @@

    응답

    경매 목록 조회

    -

    요청

    +

    요청

    GET /auctions?size=10 HTTP/1.1
    -Content-Type: application/json
    +Content-Type: application/json +Authorization: Bearer accessToken
    @@ -1360,9 +1607,27 @@

    요청

    + ++++ + + + + + + + + + + + + +
    NameDescription

    Authorization

    회원 Bearer 인증 정보

    -

    응답

    +

    응답

    HTTP/1.1 200 OK
    @@ -1371,17 +1636,17 @@ 

    응답

    { "auctions" : [ { "id" : 2, - "title" : "경매 상품 2", + "title" : "경매 상품 1", "image" : "http://localhost:8080/auctions/images/1", "auctionPrice" : 1000, - "status" : "FAILURE", + "status" : "UNBIDDEN", "auctioneerCount" : 2 }, { "id" : 1, "title" : "경매 상품 1", "image" : "http://localhost:8080/auctions/images/1", "auctionPrice" : 1000, - "status" : "FAILURE", + "status" : "UNBIDDEN", "auctioneerCount" : 2 } ], "isLast" : true @@ -1452,18 +1717,17 @@

    응답

    -

    경매 상세 조회

    +

    자신이 등록한 경매 목록 조회

    -

    요청

    +

    요청

    -
    GET /auctions/1 HTTP/1.1
    +
    GET /users/auctions/mine?size=10&page=1 HTTP/1.1
     Content-Type: application/json
     Authorization: Bearer accessToken
    -@@ -1476,53 +1740,58 @@

    요청

    - - + + + + + + + + +
    Table 4. /auctions/{auctionId}

    auctionId

    조회하고자 하는 경매 ID

    size

    페이지 크기

    page

    페이지 번호

    + ++++ + + + + + + + + + +
    NameDescription

    Authorization

    회원 Bearer 인증 정보

    -

    응답

    +

    응답

    HTTP/1.1 200 OK
     Content-Type: application/json
     
     {
    -  "auction" : {
    +  "auctions" : [ {
    +    "id" : 2,
    +    "title" : "경매 상품 2",
    +    "image" : "http://localhost:8080/auctions/images/1",
    +    "auctionPrice" : 1000,
    +    "status" : "UNBIDDEN",
    +    "auctioneerCount" : 2
    +  }, {
         "id" : 1,
    -    "images" : [ "http://localhost:8080/auctions/images/1" ],
         "title" : "경매 상품 1",
    -    "category" : {
    -      "main" : "main",
    -      "sub" : "sub"
    -    },
    -    "description" : "이것은 경매 상품 1 입니다.",
    -    "startPrice" : 1000,
    -    "lastBidPrice" : null,
    -    "status" : "FAILURE",
    -    "bidUnit" : 1000,
    -    "registerTime" : "2023-08-16T16:30:53",
    -    "closingTime" : "2023-08-16T16:30:53",
    -    "directRegions" : [ {
    -      "first" : "서울특별시",
    -      "second" : "강서구",
    -      "third" : "역삼동"
    -    } ],
    +    "image" : "http://localhost:8080/auctions/images/1",
    +    "auctionPrice" : 1000,
    +    "status" : "UNBIDDEN",
         "auctioneerCount" : 2
    -  },
    -  "seller" : {
    -    "id" : 1,
    -    "image" : "https://profile.com",
    -    "nickname" : "판매자",
    -    "reliability" : 3.5
    -  },
    -  "chat" : {
    -    "id" : 1,
    -    "isChatParticipant" : true
    -  },
    -  "isOwner" : true
    +  } ],
    +  "isLast" : true
     }
    @@ -1541,157 +1810,1622 @@

    응답

    -

    auction.id

    -

    Number

    -

    경매 글 ID

    +

    auctions

    +

    Array

    +

    조회한 경매 목록

    -

    auction.images

    +

    auctions.[]

    Array

    -

    경매 이미지

    +

    조회한 단일 경매 정보

    -

    auction.title

    -

    String

    -

    경매 글 제목

    +

    auctions.[].id

    +

    Number

    +

    경매 ID

    -

    auction.category

    -

    Object

    -

    경매 카테고리

    +

    auctions.[].title

    +

    String

    +

    경매 글 제목

    -

    auction.category.main

    +

    auctions.[].image

    String

    -

    상위 카테고리

    +

    경매 대표 이미지

    -

    auction.category.sub

    -

    String

    -

    하위 카테고리

    +

    auctions.[].auctionPrice

    +

    Number

    +

    경매가(시작가, 현재가, 낙찰가 중 하나)

    -

    auction.description

    +

    auctions.[].status

    String

    -

    경매 본문

    +

    경매 상태

    -

    auction.startPrice

    +

    auctions.[].auctioneerCount

    Number

    -

    시작가

    +

    경매 참여자 수

    -

    auction.lastBidPrice

    -

    Null

    -

    마지막 입찰가

    +

    isLast

    +

    Boolean

    +

    마지막 페이지 여부

    + + + +
    +
    +
    +

    자신이 참여한 경매 목록 조회

    +
    +

    요청

    +
    +
    +
    GET /users/auctions/bids?size=10&page=1 HTTP/1.1
    +Content-Type: application/json
    +Authorization: Bearer accessToken
    +
    +
    + ++++ + + + + + + - - - + + - - - + + + +
    ParameterDescription

    auction.status

    String

    경매 상태

    size

    페이지 크기

    auction.bidUnit

    Number

    입찰 단위

    page

    페이지 번호

    + ++++ + - - - + + + + - - - + + + + +

    auction.registerTime

    String

    경매 등록시간

    NameDescription

    auction.closingTime

    String

    경매 마감시간

    Authorization

    회원 Bearer 인증 정보

    +
    +
    +

    응답

    +
    +
    +
    HTTP/1.1 200 OK
    +Content-Type: application/json
    +
    +{
    +  "auctions" : [ {
    +    "id" : 2,
    +    "title" : "경매 상품 2",
    +    "image" : "http://localhost:8080/auctions/images/1",
    +    "auctionPrice" : 1000,
    +    "status" : "UNBIDDEN",
    +    "auctioneerCount" : 2
    +  }, {
    +    "id" : 1,
    +    "title" : "경매 상품 1",
    +    "image" : "http://localhost:8080/auctions/images/1",
    +    "auctionPrice" : 1000,
    +    "status" : "UNBIDDEN",
    +    "auctioneerCount" : 2
    +  } ],
    +  "isLast" : true
    +}
    +
    +
    + +++++ + + + + + + + - + - + - + - + - - - + + + - + - + - + - + - + - + - - - + + + - + - + + + + + + + + +
    PathTypeDescription

    auction.directRegions

    auctions

    Array

    모든 직거래 지역

    조회한 경매 목록

    auction.directRegions.[]

    auctions.[]

    Array

    단일 직거래 지역

    조회한 단일 경매 정보

    auction.directRegions.[].first

    String

    첫 번째 직거래 지역

    auctions.[].id

    Number

    경매 ID

    auction.directRegions.[].second

    auctions.[].title

    String

    두 번째 직거래 지역

    경매 글 제목

    auction.directRegions.[].third

    auctions.[].image

    String

    세 번째 직거래 지역

    경매 대표 이미지

    auction.auctioneerCount

    auctions.[].auctionPrice

    Number

    경매 참여자 수

    경매가(시작가, 현재가, 낙찰가 중 하나)

    seller

    Object

    판매자 정보

    auctions.[].status

    String

    경매 상태

    seller.id

    auctions.[].auctioneerCount

    Number

    판매자 ID

    경매 참여자 수

    isLast

    Boolean

    마지막 페이지 여부

    +
    +
    +
    +

    경매 상세 조회

    +
    +

    요청

    +
    +
    +
    GET /auctions/1 HTTP/1.1
    +Content-Type: application/json
    +Authorization: Bearer accessToken
    +
    +
    + + ++++ + + + + + + + + + + + + +
    Table 5. /auctions/{auctionId}
    ParameterDescription

    auctionId

    조회하고자 하는 경매 ID

    + ++++ + + + + + + + + + + + + +
    NameDescription

    Authorization

    회원 Bearer 인증 정보

    +
    +
    +

    응답

    +
    +
    +
    HTTP/1.1 200 OK
    +Content-Type: application/json
    +
    +{
    +  "auction" : {
    +    "id" : 1,
    +    "images" : [ "http://localhost:8080/auctions/images/1" ],
    +    "title" : "경매 상품 1",
    +    "category" : {
    +      "main" : "main",
    +      "sub" : "sub"
    +    },
    +    "description" : "이것은 경매 상품 1 입니다.",
    +    "startPrice" : 1000,
    +    "lastBidPrice" : null,
    +    "status" : "UNBIDDEN",
    +    "bidUnit" : 1000,
    +    "registerTime" : "2023-10-09T11:02:42",
    +    "closingTime" : "2023-10-09T11:02:42",
    +    "directRegions" : [ {
    +      "first" : "서울특별시",
    +      "second" : "강서구",
    +      "third" : "역삼동"
    +    } ],
    +    "auctioneerCount" : 2
    +  },
    +  "seller" : {
    +    "id" : 1,
    +    "image" : "http://localhost:8080/users/images/1",
    +    "nickname" : "판매자",
    +    "reliability" : 3.5
    +  },
    +  "chat" : {
    +    "id" : 1,
    +    "isChatParticipant" : true
    +  },
    +  "isOwner" : true
    +}
    +
    +
    + +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PathTypeDescription

    auction.id

    Number

    경매 글 ID

    auction.images

    Array

    경매 이미지

    auction.title

    String

    경매 글 제목

    auction.category

    Object

    경매 카테고리

    auction.category.main

    String

    상위 카테고리

    auction.category.sub

    String

    하위 카테고리

    auction.description

    String

    경매 본문

    auction.startPrice

    Number

    시작가

    auction.lastBidPrice

    Null

    마지막 입찰가

    auction.status

    String

    경매 상태

    auction.bidUnit

    Number

    입찰 단위

    auction.registerTime

    String

    경매 등록시간

    auction.closingTime

    String

    경매 마감시간

    auction.directRegions

    Array

    모든 직거래 지역

    auction.directRegions.[]

    Array

    단일 직거래 지역

    auction.directRegions.[].first

    String

    첫 번째 직거래 지역

    auction.directRegions.[].second

    String

    두 번째 직거래 지역

    auction.directRegions.[].third

    String

    세 번째 직거래 지역

    auction.auctioneerCount

    Number

    경매 참여자 수

    seller

    Object

    판매자 정보

    seller.id

    Number

    판매자 ID

    seller.image

    String

    판매자 프로필 이미지 주소

    seller.nickname

    String

    판매자 닉네임

    seller.reliability

    Number

    판매자 신뢰도

    chat.id

    Number

    채팅방 ID

    chat.isChatParticipant

    Boolean

    채팅방을 생성 가능 유저 여부

    isOwner

    Boolean

    유저가 해당 경매 글을 작성한 유저인지에 대한 여부

    +
    +
    +
    +

    경매 삭제

    +
    +

    요청

    +
    +
    +
    DELETE /auctions/1 HTTP/1.1
    +Content-Type: application/json
    +Authorization: Bearer accessToken
    +
    +
    + + ++++ + + + + + + + + + + + + +
    Table 6. /auctions/{auctionId}
    ParameterDescription

    auctionId

    삭제할 경매 ID

    + ++++ + + + + + + + + + + + + +
    NameDescription

    Authorization

    회원 Bearer 인증 정보

    +
    +
    +

    응답

    +
    +
    +
    HTTP/1.1 204 No Content
    +
    +
    +
    +
    +
    +
    +
    +

    Q&A API

    +
    +
    +

    질문 등록

    +
    +

    요청

    +
    +
    +
    POST /questions HTTP/1.1
    +Content-Type: application/json
    +Authorization: Bearer accessToken
    +
    +{
    +  "auctionId" : 1,
    +  "content" : "궁금한 점이 있습니다."
    +}
    +
    +
    + ++++ + + + + + + + + + + + + +
    NameDescription

    Authorization

    회원 Bearer 인증 정보

    + +++++ + + + + + + + + + + + + + + + + + + + +
    PathTypeDescription

    auctionId

    Number

    질문할 경매 ID

    content

    String

    질문 내용

    +
    +
    +

    응답

    +
    +
    +
    HTTP/1.1 201 Created
    +Location: /auctions/1
    +
    +
    +
    +
    +
    +

    질문 삭제

    +
    +

    요청

    +
    +
    +
    DELETE /questions/1 HTTP/1.1
    +Authorization: Bearer accessToken
    +
    +
    + ++++ + + + + + + + + + + + + +
    NameDescription

    Authorization

    회원 Bearer 인증 정보

    + + ++++ + + + + + + + + + + + + +
    Table 7. /questions/{questionId}
    ParameterDescription

    questionId

    삭제할 질문 ID

    +
    +
    +

    응답

    +
    +
    +
    HTTP/1.1 204 No Content
    +
    +
    +
    +
    +
    +

    답변 등록

    +
    +

    요청

    +
    +
    +
    POST /questions/1/answers HTTP/1.1
    +Content-Type: application/json
    +Authorization: Bearer accessToken
    +
    +{
    +  "auctionId" : 1,
    +  "content" : "답변 드립니다."
    +}
    +
    +
    + ++++ + + + + + + + + + + + + +
    NameDescription

    Authorization

    회원 Bearer 인증 정보

    + +++++ + + + + + + + + + + + + + + + + + + + +
    PathTypeDescription

    auctionId

    Number

    답변할 질문의 경매 ID

    content

    String

    답변 내용

    +
    +
    +

    응답

    +
    +
    +
    HTTP/1.1 201 Created
    +Location: /auctions/1
    +
    +
    +
    +
    +
    +

    답변 삭제

    +
    +

    요청

    +
    +
    +
    DELETE /questions/answers/1 HTTP/1.1
    +Authorization: Bearer accessToken
    +
    +
    + ++++ + + + + + + + + + + + + +
    NameDescription

    Authorization

    회원 Bearer 인증 정보

    + + ++++ + + + + + + + + + + + + +
    Table 8. /questions/answers/{answerId}
    ParameterDescription

    answerId

    삭제할 답변 ID

    +
    +
    +

    응답

    +
    +
    +
    HTTP/1.1 204 No Content
    +
    +
    +
    +
    +
    +

    Q&A 조회

    +
    +

    요청

    +
    +
    +
    GET /auctions/1/questions HTTP/1.1
    +Content-Type: application/json
    +Authorization: Bearer accessToken
    +
    +
    +
    +
    +

    응답

    +
    +
    +
    HTTP/1.1 200 OK
    +Content-Type: application/json
    +
    +{
    +  "qnas" : [ {
    +    "question" : {
    +      "id" : 1,
    +      "writer" : {
    +        "id" : 1,
    +        "name" : "질문자",
    +        "image" : "http://localhost:8080/users/images/1"
    +      },
    +      "createdTime" : "2023-10-09T11:02:42",
    +      "content" : "질문1"
    +    },
    +    "answer" : {
    +      "id" : 1,
    +      "writer" : {
    +        "id" : 2,
    +        "name" : "판매자",
    +        "image" : "http://localhost:8080/users/images/2"
    +      },
    +      "createdTime" : "2023-10-09T11:02:42",
    +      "content" : "답변1"
    +    }
    +  }, {
    +    "question" : {
    +      "id" : 2,
    +      "writer" : {
    +        "id" : 1,
    +        "name" : "질문자",
    +        "image" : "http://localhost:8080/users/images/1"
    +      },
    +      "createdTime" : "2023-10-09T11:02:42",
    +      "content" : "질문2"
    +    },
    +    "answer" : {
    +      "id" : 2,
    +      "writer" : {
    +        "id" : 2,
    +        "name" : "판매자",
    +        "image" : "http://localhost:8080/users/images/2"
    +      },
    +      "createdTime" : "2023-10-09T11:02:42",
    +      "content" : "답변1"
    +    }
    +  } ]
    +}
    +
    +
    + +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PathTypeDescription

    qnas.[]

    Array

    모든 경매 질문과 답변 목록

    qnas.[].question

    Object

    질문 정보 JSON

    qnas.[].question.id

    Number

    경매 질문 글 ID

    qnas.[].question.writer

    Object

    질문자 정보 JSON

    qnas.[].question.writer.id

    Number

    질문자의 ID

    qnas.[].question.writer.name

    String

    질문자의 이름

    qnas.[].question.writer.image

    String

    질문자의 프로필 이미지 URL

    qnas.[].question.createdTime

    String

    질문 등록 시간

    qnas.[].question.content

    String

    질문 내용

    qnas.[].answer

    Object

    답변 정보 JSON

    qnas.[].answer.id

    Number

    경매 답변 글 ID

    qnas.[].answer.writer

    Object

    답변자 정보 JSON

    qnas.[].answer.writer.id

    Number

    답변자의 ID

    qnas.[].answer.writer.name

    String

    답변자의 이름

    qnas.[].answer.writer.image

    String

    답변자의 프로필 이미지 URL

    qnas.[].answer.createdTime

    String

    답변 등록 시간

    qnas.[].answer.content

    String

    답변 내용

    +
    +
    +
    +
    +
    +

    입찰 API

    +
    +
    +

    입찰 등록

    +
    +

    요청

    +
    +
    +
    POST /bids HTTP/1.1
    +Content-Type: application/json
    +Authorization: Bearer accessToken
    +
    +{
    +  "auctionId" : 1,
    +  "bidPrice" : 10000
    +}
    +
    +
    + ++++ + + + + + + + + + + + + +
    NameDescription

    Authorization

    회원 Bearer 인증 정보

    + +++++ + + + + + + + + + + + + + + + + + + + +
    PathTypeDescription

    auctionId

    Number

    입찰할 경매 ID

    bidPrice

    Number

    입찰 금액

    +
    +
    +

    응답

    +
    +
    +
    HTTP/1.1 201 Created
    +Location: /auctions/1
    +
    +
    +
    +
    +
    +

    입찰 조회

    +
    +

    요청

    +
    +
    +
    GET /bids/-999 HTTP/1.1
    +Content-Type: application/json
    +
    +
    +
    +
    +

    응답

    +
    +
    +
    HTTP/1.1 200 OK
    +Content-Type: application/json
    +
    +{
    +  "bids" : [ {
    +    "name" : "사용자1",
    +    "profileImage" : "http://localhost:8080/users/images/1",
    +    "price" : 10000,
    +    "bidTime" : "2023-10-09T11:02:50"
    +  }, {
    +    "name" : "사용자2",
    +    "profileImage" : "http://localhost:8080/users/images/2",
    +    "price" : 12000,
    +    "bidTime" : "2023-10-09T11:02:50"
    +  } ]
    +}
    +
    +
    + +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PathTypeDescription

    bids.[]

    Array

    특정 경매의 모든 입찰 목록

    bids.[].name

    String

    입찰한 사용자의 닉네임

    bids.[].profileImage

    String

    입찰한 사용자의 프로필 이미지 URL

    bids.[].price

    Number

    입찰한 금액

    bids.[].bidTime

    String

    입찰한 시간

    +
    +
    +
    +
    +
    +

    채팅 API

    +
    +
    +

    채팅방 등록

    +
    +

    요청

    +
    +
    +
    POST /chattings HTTP/1.1
    +Content-Type: application/json
    +Authorization: Bearer accessToken
    +
    +{
    +  "auctionId" : 1
    +}
    +
    +
    + ++++ + + + + + + + + + + + + +
    NameDescription

    Authorization

    회원 Bearer 인증 정보

    + +++++ + + + + + + + + + + + + + + +
    PathTypeDescription

    auctionId

    Number

    연관된 경매 ID

    +
    +
    +

    응답

    +
    +
    +
    HTTP/1.1 201 Created
    +Location: /chattings/1
    +Content-Type: application/json
    +
    +{
    +  "chatRoomId" : 1
    +}
    +
    +
    + +++++ + + + + + + + + + + + + + + +
    PathTypeDescription

    chatRoomId

    Number

    생성된 채팅방 ID

    +
    +
    +
    +

    채팅방 목록 조회

    +
    +

    요청

    +
    +
    +
    GET /chattings HTTP/1.1
    +Content-Type: application/json
    +Authorization: Bearer accessToken
    +
    +
    + ++++ + + + + + + + + + + + + +
    NameDescription

    Authorization

    회원 Bearer 인증 정보

    +
    +
    +

    응답

    +
    +
    +
    HTTP/1.1 200 OK
    +Content-Type: application/json
    +
    +[ {
    +  "id" : 1,
    +  "chatPartner" : {
    +    "id" : 2,
    +    "name" : "구매자1",
    +    "profileImage" : "http://localhost:8080/users/images/2"
    +  },
    +  "auction" : {
    +    "id" : 1,
    +    "title" : "경매1",
    +    "image" : "http://localhost:8080/auctions/images/1",
    +    "price" : 10000
    +  },
    +  "lastMessage" : {
    +    "createdAt" : "2023-10-09T11:02:52",
    +    "contents" : "메시지1"
    +  },
    +  "isChatAvailable" : true
    +}, {
    +  "id" : 2,
    +  "chatPartner" : {
    +    "id" : 3,
    +    "name" : "구매자2",
    +    "profileImage" : "http://localhost:8080/users/images/3"
    +  },
    +  "auction" : {
    +    "id" : 2,
    +    "title" : "경매2",
    +    "image" : "http://localhost:8080/auctions/images/1",
    +    "price" : 20000
    +  },
    +  "lastMessage" : {
    +    "createdAt" : "2023-10-09T11:02:52",
    +    "contents" : "메시지2"
    +  },
    +  "isChatAvailable" : true
    +} ]
    +
    +
    + +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PathTypeDescription

    []

    Array

    자신이 참여한 채팅방 목록

    [].id

    Number

    채팅방 ID

    [].chatPartner

    Object

    채팅 상대방

    [].chatPartner.id

    Number

    채팅 상대방 ID

    [].chatPartner.name

    String

    채팅 상대방 이름

    [].chatPartner.profileImage

    String

    채팅 상대방 프로필 사진

    [].auction

    Object

    채팅방과 연관된 경매

    [].auction.id

    Number

    경매 ID

    [].auction.title

    String

    경매 제목

    [].auction.image

    String

    경매 대표 사진

    [].auction.price

    Number

    낙찰가

    [].lastMessage

    Object

    마지막으로 전송된 메시지

    [].lastMessage.createdAt

    String

    메시지를 보낸 시간

    [].lastMessage.contents

    String

    메시지 내용

    [].isChatAvailable

    Boolean

    채팅 가능 여부

    +
    +
    +
    +

    채팅방 상세 조회

    +
    +

    요청

    +
    +
    +
    GET /chattings/1 HTTP/1.1
    +Content-Type: application/json
    +Authorization: Bearer accessToken
    +
    +
    + + ++++ + + + + + + + + + + + + +
    Table 9. /chattings/{chatRoomId}
    ParameterDescription

    chatRoomId

    조회하고자 하는 채팅방 ID

    + ++++ + + + + + + + + + + + + +
    NameDescription

    Authorization

    회원 Bearer 인증 정보

    +
    +
    +

    응답

    +
    +
    +
    HTTP/1.1 200 OK
    +Content-Type: application/json
    +
    +{
    +  "id" : 1,
    +  "chatPartner" : {
    +    "id" : 1,
    +    "name" : "판매자",
    +    "profileImage" : "http://localhost:8080/users/images/1"
    +  },
    +  "auction" : {
    +    "id" : 1,
    +    "title" : "경매1",
    +    "image" : "http://localhost:8080/auctions/images/1",
    +    "price" : 10000
    +  },
    +  "isChatAvailable" : true
    +}
    +
    +
    + +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PathTypeDescription

    id

    Number

    채팅방 ID

    auction

    Object

    채팅방과 연관된 경매

    auction.id

    Number

    경매 ID

    auction.title

    String

    경매 제목

    auction.image

    String

    경매 대표 사진

    auction.price

    Number

    낙찰가

    chatPartner

    Object

    채팅 상대방

    chatPartner.id

    Number

    채팅 상대방 ID

    chatPartner.name

    String

    채팅 상대방 이름

    chatPartner.profileImage

    String

    채팅 상대방 프로필 사진

    isChatAvailable

    Boolean

    채팅 가능 여부

    +
    +
    +
    +

    메시지 전송

    +
    +

    요청

    +
    +
    +
    POST /chattings/1/messages HTTP/1.1
    +Content-Type: application/json
    +Authorization: Bearer accessToken
    +
    +{
    +  "receiverId" : 1,
    +  "contents" : "메시지 내용"
    +}
    +
    +
    + + ++++ + + + + + + + + + + + + +
    Table 10. /chattings/{chatRoomId}/messages
    ParameterDescription

    chatRoomId

    메시지를 보내고 싶은 채팅방의 ID

    + ++++ + + + + + + - - - + + + +
    NameDescription

    seller.image

    String

    판매자 프로필 이미지 주소

    Authorization

    회원 Bearer 인증 정보

    + +++++ + - - - + + + + + - + - + - - - + + + + +

    seller.nickname

    String

    판매자 닉네임

    PathTypeDescription

    seller.reliability

    receiverId

    Number

    판매자 신뢰도

    메시지 수신자 ID

    chat.id

    Number

    채팅방 ID

    contents

    String

    메시지 내용

    +
    +
    +

    응답

    +
    +
    +
    HTTP/1.1 201 Created
    +Location: /chattings/1
    +Content-Type: application/json
    +
    +{
    +  "id" : 1
    +}
    +
    +
    + +++++ + - - - + + + + + - - - + + +

    chat.isChatParticipant

    Boolean

    채팅방을 생성 가능 유저 여부

    PathTypeDescription

    isOwner

    Boolean

    유저가 해당 경매 글을 작성한 유저인지에 대한 여부

    id

    Number

    메시지 보내진 채팅방 ID

    -

    경매 삭제

    +

    메시지 목록 조회

    -

    요청

    +

    요청

    -
    DELETE /auctions/1 HTTP/1.1
    +
    GET /chattings/1/messages?lastMessageId=1 HTTP/1.1
     Content-Type: application/json
     Authorization: Bearer accessToken
    - +@@ -1704,8 +3438,8 @@

    요청

    - - + +
    Table 5. /auctions/{auctionId}Table 11. /chattings/{chatRoomId}/messages

    auctionId

    삭제할 경매 ID

    chatRoomId

    메시지를 보내고 싶은 채팅방의 ID

    @@ -1727,34 +3461,101 @@

    요청

    + ++++ + + + + + + + + + + + + +
    ParameterDescription

    lastMessageId

    마지막으로 응답받은 메시지의 ID

    -

    응답

    +

    응답

    -
    HTTP/1.1 204 No Content
    +
    HTTP/1.1 200 OK
    +Content-Type: application/json
    +
    +[ {
    +  "id" : 1,
    +  "createdAt" : "2023-10-09T11:02:52",
    +  "isMyMessage" : true,
    +  "contents" : "메시지내용"
    +} ]
    + +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PathTypeDescription

    []

    Array

    하나의 채팅방 내의 메시지 목록 (lastMessageId가 포함되어 있다면 lastMessageId 이후의 메시지 목록

    [].id

    Number

    메시지 ID

    [].createdAt

    String

    메시지를 보낸 시간

    [].isMyMessage

    Boolean

    조회를 요청한 사람이 보낸 메시지인지 여부

    [].contents

    String

    메시지 내용

    -

    입찰 API

    +

    신고 API

    -

    입찰 등록

    +

    경매 신고 등록

    -

    요청

    +

    요청

    -
    POST /bids HTTP/1.1
    +
    POST /reports/auctions HTTP/1.1
     Content-Type: application/json
     Authorization: Bearer accessToken
     
     {
       "auctionId" : 1,
    -  "bidPrice" : 10000
    +  "description" : "신고합니다"
     }
    @@ -1793,18 +3594,18 @@

    요청

    auctionId

    Number

    -

    입찰할 경매 ID

    +

    신고할 경매 ID

    -

    bidPrice

    -

    Number

    -

    입찰 금액

    +

    description

    +

    String

    +

    신고 내용

    -

    응답

    +

    응답

    HTTP/1.1 201 Created
    @@ -1814,34 +3615,60 @@ 

    응답

    -

    입찰 조회

    +

    경매 신고 조회

    -

    요청

    +

    요청

    -
    GET /bids/1 HTTP/1.1
    +
    GET /reports/auctions HTTP/1.1
     Content-Type: application/json
    -

    응답

    +

    응답

    HTTP/1.1 200 OK
     Content-Type: application/json
     
     {
    -  "bids" : [ {
    -    "name" : "사용자1",
    -    "profileImage" : "이미지1",
    -    "price" : 10000,
    -    "bidTime" : "2023-08-16T16:30:54"
    +  "reports" : [ {
    +    "id" : 1,
    +    "reporter" : {
    +      "id" : 2,
    +      "name" : "회원1"
    +    },
    +    "createdTime" : "2023-10-09T11:03:02",
    +    "auction" : {
    +      "id" : 1,
    +      "title" : "제목"
    +    },
    +    "description" : "신고합니다."
       }, {
    -    "name" : "사용자2",
    -    "profileImage" : "이미지2",
    -    "price" : 12000,
    -    "bidTime" : "2023-08-16T16:30:54"
    +    "id" : 2,
    +    "reporter" : {
    +      "id" : 3,
    +      "name" : "회원2"
    +    },
    +    "createdTime" : "2023-10-09T11:03:02",
    +    "auction" : {
    +      "id" : 1,
    +      "title" : "제목"
    +    },
    +    "description" : "신고합니다."
    +  }, {
    +    "id" : 3,
    +    "reporter" : {
    +      "id" : 4,
    +      "name" : "회원3"
    +    },
    +    "createdTime" : "2023-10-09T11:03:02",
    +    "auction" : {
    +      "id" : 1,
    +      "title" : "제목"
    +    },
    +    "description" : "신고합니다."
       } ]
     }
    @@ -1861,51 +3688,62 @@

    응답

    -

    bids.[]

    +

    reports.[]

    Array

    -

    특정 경매의 모든 입찰 목록

    +

    모든 경매 신고 목록

    -

    bids.[].name

    +

    reports.[].id

    +

    Number

    +

    경매 신고 글 ID

    + + +

    reports.[].reporter.id

    +

    Number

    +

    경매 신고한 사용자의 ID

    + + +

    reports.[].reporter.name

    String

    -

    입찰한 사용자의 닉네임

    +

    경매 신고한 사용자의 이름

    -

    bids.[].profileImage

    +

    reports.[].createdTime

    String

    -

    입찰한 사용자의 프로필 이미지 URL

    +

    경매 신고 시간

    -

    bids.[].price

    +

    reports.[].auction.id

    Number

    -

    입찰한 금액

    +

    신고한 경매 ID

    -

    bids.[].bidTime

    +

    reports.[].auction.title

    String

    -

    입찰한 시간

    +

    신고한 경매 제목

    + + +

    reports.[].description

    +

    String

    +

    신고 내용

    -
    -
    -
    -

    채팅 API

    -
    -

    채팅방 등록

    +

    채팅방 신고 등록

    -

    요청

    +

    요청

    -
    POST /chattings HTTP/1.1
    +
    POST /reports/chat-rooms HTTP/1.1
     Content-Type: application/json
     Authorization: Bearer accessToken
     
     {
    -  "auctionId" : 1
    +  "chatRoomId" : 1,
    +  "description" : "신고합니다"
     }
    @@ -1942,123 +3780,82 @@

    요청

    -

    auctionId

    +

    chatRoomId

    Number

    -

    연관된 경매 ID

    +

    신고할 채팅방 ID

    + + +

    description

    +

    String

    +

    신고 내용

    -

    응답

    +

    응답

    HTTP/1.1 201 Created
    -Location: /chattings/1
    -Content-Type: application/json
    -
    -{
    -  "chatRoomId" : 1
    -}
    +Location: /chattings/1
    - ----- - - - - - - - - - - - - - - -
    PathTypeDescription

    chatRoomId

    Number

    생성된 채팅방 ID

    -

    채팅방 목록 조회

    +

    채팅방 신고 조회

    -

    요청

    +

    요청

    -
    GET /chattings HTTP/1.1
    -Content-Type: application/json
    -Authorization: Bearer accessToken
    +
    GET /reports/chat-rooms HTTP/1.1
    +Content-Type: application/json
    - ---- - - - - - - - - - - - - -
    NameDescription

    Authorization

    회원 Bearer 인증 정보

    -

    응답

    +

    응답

    HTTP/1.1 200 OK
     Content-Type: application/json
     
    -[ {
    -  "id" : 1,
    -  "chatPartner" : {
    -    "id" : 2,
    -    "name" : "사용자2",
    -    "profileImage" : "profile.png"
    -  },
    -  "auction" : {
    +{
    +  "reports" : [ {
         "id" : 1,
    -    "title" : "경매1",
    -    "image" : "http://localhost:8080/auctions/images/1",
    -    "price" : 10000
    -  },
    -  "lastMessage" : {
    -    "createdAt" : "2023-08-16T16:30:55",
    -    "contents" : "메시지1"
    -  },
    -  "isChatAvailable" : true
    -}, {
    -  "id" : 2,
    -  "chatPartner" : {
    -    "id" : 3,
    -    "name" : "사용자3",
    -    "profileImage" : "profile.png"
    -  },
    -  "auction" : {
    +    "reporter" : {
    +      "id" : 2,
    +      "name" : "구매자1"
    +    },
    +    "createdTime" : "2023-10-09T11:03:02",
    +    "chatRoom" : {
    +      "id" : 1
    +    },
    +    "description" : "신고합니다."
    +  }, {
         "id" : 2,
    -    "title" : "경매2",
    -    "image" : "http://localhost:8080/auctions/images/1",
    -    "price" : 20000
    -  },
    -  "lastMessage" : {
    -    "createdAt" : "2023-08-16T16:30:55",
    -    "contents" : "메시지2"
    -  },
    -  "isChatAvailable" : true
    -} ]
    + "reporter" : { + "id" : 3, + "name" : "구매자2" + }, + "createdTime" : "2023-10-09T11:03:02", + "chatRoom" : { + "id" : 1 + }, + "description" : "신고합니다." + }, { + "id" : 3, + "reporter" : { + "id" : 3, + "name" : "구매자2" + }, + "createdTime" : "2023-10-09T11:03:02", + "chatRoom" : { + "id" : 1 + }, + "description" : "신고합니다." + } ] +}

    @@ -2076,154 +3873,174 @@

    응답

    - + - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - + - + - + - + - + - + - + - + - - - - - - - - - - - + - + - - - - - - +

    []

    reports.[]

    Array

    자신이 참여한 채팅방 목록

    [].id

    Number

    채팅방 ID

    [].chatPartner

    Object

    채팅 상대방

    모든 채팅방 신고 목록

    [].chatPartner.id

    reports.[].id

    Number

    채팅 상대방 ID

    [].chatPartner.name

    String

    채팅 상대방 이름

    [].chatPartner.profileImage

    String

    채팅 상대방 프로필 사진

    [].auction

    Object

    채팅방과 연관된 경매

    채팅방 신고 글 ID

    [].auction.id

    reports.[].reporter.id

    Number

    경매 ID

    채팅방을 신고한 사용자의 ID

    [].auction.title

    reports.[].reporter.name

    String

    경매 제목

    채팅방을 신고한 사용자의 이름

    [].auction.image

    reports.[].createdTime

    String

    경매 대표 사진

    채팅방 신고 시간

    [].auction.price

    reports.[].chatRoom.id

    Number

    낙찰가

    [].lastMessage

    Object

    마지막으로 전송된 메시지

    [].lastMessage.createdAt

    String

    메시지를 보낸 시간

    신고한 채팅방 ID

    [].lastMessage.contents

    reports.[].description

    String

    메시지 내용

    [].isChatAvailable

    Boolean

    채팅 가능 여부

    신고 내용

    -

    채팅방 상세 조회

    +

    질문 신고 등록

    -

    요청

    +

    요청

    -
    GET /chattings/1 HTTP/1.1
    +
    POST /reports/questions HTTP/1.1
     Content-Type: application/json
    -Authorization: Bearer accessToken
    +Authorization: Bearer accessToken + +{ + "auctionId" : 1, + "questionId" : 1, + "description" : "신고합니다." +}
    - - + - - + +
    Table 6. /chattings/{chatRoomId}
    ParameterName Description

    chatRoomId

    조회하고자 하는 채팅방 ID

    Authorization

    회원 Bearer 인증 정보

    --+++ - + + - - + + + + + + + + + + + + +
    NamePathType Description

    Authorization

    회원 Bearer 인증 정보

    auctionId

    Number

    질문의 경매 ID

    questionId

    Number

    신고할 질문 ID

    description

    String

    신고 내용

    -

    응답

    +

    응답

    +
    +
    +
    HTTP/1.1 201 Created
    +Location: /auctions/1/questions
    +
    +
    +
    +
    +
    +

    채팅방 신고 조회

    +
    +

    요청

    +
    +
    +
    GET /reports/questions HTTP/1.1
    +Content-Type: application/json
    +
    +
    +
    +
    +

    응답

    HTTP/1.1 200 OK
     Content-Type: application/json
     
     {
    -  "id" : 1,
    -  "chatPartner" : {
    +  "reports" : [ {
    +    "id" : 1,
    +    "reporter" : {
    +      "id" : 2,
    +      "name" : "구매자1"
    +    },
    +    "createdTime" : "2023-10-09T11:03:02",
    +    "question" : {
    +      "id" : 1
    +    },
    +    "description" : "신고합니다."
    +  }, {
         "id" : 2,
    -    "name" : "채팅 상대방",
    -    "profileImage" : "profile.png"
    -  },
    -  "auction" : {
    -    "id" : 1,
    -    "title" : "경매 상품 1",
    -    "image" : "http://localhost:8080/auctions/images/1",
    -    "price" : 3000
    -  },
    -  "isChatAvailable" : true
    +    "reporter" : {
    +      "id" : 2,
    +      "name" : "구매자1"
    +    },
    +    "createdTime" : "2023-10-09T11:03:02",
    +    "question" : {
    +      "id" : 2
    +    },
    +    "description" : "신고합니다."
    +  }, {
    +    "id" : 3,
    +    "reporter" : {
    +      "id" : 2,
    +      "name" : "구매자1"
    +    },
    +    "createdTime" : "2023-10-09T11:03:02",
    +    "question" : {
    +      "id" : 3
    +    },
    +    "description" : "신고합니다."
    +  } ]
     }
    @@ -2242,100 +4059,62 @@

    응답

    -

    id

    -

    Number

    -

    채팅방 ID

    +

    reports.[]

    +

    Array

    +

    모든 질문 신고 목록

    -

    auction

    -

    Object

    -

    채팅방과 연관된 경매

    +

    reports.[].id

    +

    Number

    +

    질문 신고 글 ID

    -

    auction.id

    +

    reports.[].reporter.id

    Number

    -

    경매 ID

    +

    질문을 신고한 사용자의 ID

    -

    auction.title

    +

    reports.[].reporter.name

    String

    -

    경매 제목

    +

    질문을 신고한 사용자의 이름

    -

    auction.image

    +

    reports.[].createdTime

    String

    -

    경매 대표 사진

    - - -

    auction.price

    -

    Number

    -

    낙찰가

    - - -

    chatPartner

    -

    Object

    -

    채팅 상대방

    +

    질문 신고 시간

    -

    chatPartner.id

    +

    reports.[].question.id

    Number

    -

    채팅 상대방 ID

    - - -

    chatPartner.name

    -

    String

    -

    채팅 상대방 이름

    +

    신고한 질문 ID

    -

    chatPartner.profileImage

    +

    reports.[].description

    String

    -

    채팅 상대방 프로필 사진

    - - -

    isChatAvailable

    -

    Boolean

    -

    채팅 가능 여부

    +

    신고 내용

    -

    메시지 전송

    +

    답변 신고 등록

    -

    요청

    +

    요청

    -
    POST /chattings/1/messages HTTP/1.1
    +
    POST /reports/answers HTTP/1.1
     Content-Type: application/json
     Authorization: Bearer accessToken
     
     {
    -  "receiverId" : 1,
    -  "contents" : "메시지 내용"
    +  "auctionId" : 1,
    +  "answerId" : 1,
    +  "description" : "신고합니다."
     }
    - ---- - - - - - - - - - - - - -
    Table 7. /chattings/{chatRoomId}/messages
    ParameterDescription

    chatRoomId

    메시지를 보내고 싶은 채팅방의 ID

    -@@ -2368,28 +4147,86 @@

    요청

    - + - + - + + + + + + - +

    receiverId

    auctionId

    Number

    메시지 수신자 ID

    질문의 경매 ID

    contents

    answerId

    Number

    신고할 답변 ID

    description

    String

    메시지 내용

    신고 내용

    -

    응답

    +

    응답

    HTTP/1.1 201 Created
    -Location: /chattings/1
    +Location: /auctions/1/questions
    +
    +
    +
    +
    +
    +

    채팅방 신고 조회

    +
    +

    요청

    +
    +
    +
    GET /reports/answers HTTP/1.1
    +Content-Type: application/json
    +
    +
    +
    +
    +

    응답

    +
    +
    +
    HTTP/1.1 200 OK
     Content-Type: application/json
     
     {
    -  "id" : 1
    +  "reports" : [ {
    +    "id" : 1,
    +    "reporter" : {
    +      "id" : 2,
    +      "name" : "구매자1"
    +    },
    +    "createdTime" : "2023-10-09T11:03:02",
    +    "answer" : {
    +      "id" : 1
    +    },
    +    "description" : "신고합니다."
    +  }, {
    +    "id" : 2,
    +    "reporter" : {
    +      "id" : 2,
    +      "name" : "구매자1"
    +    },
    +    "createdTime" : "2023-10-09T11:03:02",
    +    "answer" : {
    +      "id" : 2
    +    },
    +    "description" : "신고합니다."
    +  }, {
    +    "id" : 3,
    +    "reporter" : {
    +      "id" : 2,
    +      "name" : "구매자1"
    +    },
    +    "createdTime" : "2023-10-09T11:03:02",
    +    "answer" : {
    +      "id" : 3
    +    },
    +    "description" : "신고합니다."
    +  } ]
     }
    @@ -2408,62 +4245,136 @@

    응답

    -

    id

    +

    reports.[]

    +

    Array

    +

    모든 답변 신고 목록

    + + +

    reports.[].id

    Number

    -

    메시지 보내진 채팅방 ID

    +

    답변 신고 글 ID

    + + +

    reports.[].reporter.id

    +

    Number

    +

    답변을 신고한 사용자의 ID

    + + +

    reports.[].reporter.name

    +

    String

    +

    답변을 신고한 사용자의 이름

    + + +

    reports.[].createdTime

    +

    String

    +

    답변 신고 시간

    + + +

    reports.[].answer.id

    +

    Number

    +

    신고한 답변 ID

    + + +

    reports.[].description

    +

    String

    +

    신고 내용

    +
    +
    +
    +

    디바이스 토큰 API

    +
    -

    메시지 목록 조회

    +

    디바이스 토큰 갱신

    -

    요청

    +

    요청

    -
    GET /chattings/1/messages?lastMessageId=1 HTTP/1.1
    +
    PATCH /device-token HTTP/1.1
     Content-Type: application/json
    -Authorization: Bearer accessToken
    +Authorization: Bearer accessToken + +{ + "deviceToken" : "newDeviceToken" +}
    - - + - - + +
    Table 8. /chattings/{chatRoomId}/messages
    ParameterName Description

    chatRoomId

    메시지를 보내고 싶은 채팅방의 ID

    Authorization

    회원 Bearer 인증 정보

    --+++ - + + - - + + +
    NamePathType Description

    Authorization

    회원 Bearer 인증 정보

    deviceToken

    String

    디바이스 토큰

    +
    +
    +

    응답

    +
    +
    +
    HTTP/1.1 200 OK
    +
    +
    +
    +
    +
    +
    +
    +

    유저 상호 평가 API

    +
    +
    +

    사용자 평가 등록

    +
    +

    요청

    +
    +
    +
    POST /reviews HTTP/1.1
    +Content-Type: application/json
    +Authorization: Bearer accessToken
    +
    +{
    +  "auctionId" : 1,
    +  "targetId" : 2,
    +  "content" : "친절하다.",
    +  "score" : 5.0
    +}
    +
    +
    @@ -2471,33 +4382,17 @@

    요청

    - + - - + +
    ParameterName Description

    lastMessageId

    마지막으로 응답받은 메시지의 ID

    Authorization

    회원 Bearer 인증 정보

    -
    -
    -

    응답

    -
    -
    -
    HTTP/1.1 200 OK
    -Content-Type: application/json
    -
    -[ {
    -  "id" : 1,
    -  "createdAt" : "2023-08-16T16:30:55",
    -  "isMyMessage" : true,
    -  "contents" : "메시지내용"
    -} ]
    -
    -
    @@ -2513,73 +4408,81 @@

    응답

    - - - - - - + - - - - - - + - - - + + + - + + + + + + - +

    []

    Array

    하나의 채팅방 내의 메시지 목록 (lastMessageId가 포함되어 있다면 lastMessageId 이후의 메시지 목록

    [].id

    auctionId

    Number

    메시지 ID

    [].createdAt

    String

    메시지를 보낸 시간

    거래한 경매 ID

    [].isMyMessage

    Boolean

    조회를 요청한 사람이 보낸 메시지인지 여부

    targetId

    Number

    평가 대상 ID

    [].contents

    score

    Number

    평가 점수

    content

    String

    메시지 내용

    평가 내용

    +
    +

    응답

    +
    +
    +
    HTTP/1.1 201 Created
    +Location: /reviews/1
    +
    -
    -

    신고 API

    -
    -

    경매 신고 등록

    +

    지정한 평가 아이디에 해당하는 평가 조회

    -

    요청

    +

    요청

    -
    POST /reports/auctions HTTP/1.1
    -Content-Type: application/json
    -Authorization: Bearer accessToken
    -
    -{
    -  "auctionId" : 1,
    -  "description" : "신고합니다"
    -}
    +
    GET /reviews/2 HTTP/1.1
    +Content-Type: application/json
    + - + - - + +
    Table 12. /reviews/{reviewId}
    NameParameter Description

    Authorization

    회원 Bearer 인증 정보

    reviewId

    평가와_관련된_경매_아이디

    +
    +
    +

    응답

    +
    +
    +
    HTTP/1.1 200 OK
    +Content-Type: application/json
    +
    +{
    +  "score" : 5.0,
    +  "content" : "친절하다."
    +}
    +
    +
    @@ -2595,85 +4498,77 @@

    요청

    - + - + - + - +

    auctionId

    score

    Number

    신고할 경매 ID

    평가 점수

    description

    content

    String

    신고 내용

    평가 내용

    -
    -

    응답

    -
    -
    -
    HTTP/1.1 201 Created
    -Location: /auctions/1
    -
    -
    -
    -

    경매 신고 조회

    +

    지정한 사용자가 받은 평가 목록 조회

    -

    요청

    +

    요청

    -
    GET /reports/auctions HTTP/1.1
    +
    GET /reviews/users/3 HTTP/1.1
     Content-Type: application/json
    + + ++++ + + + + + + + + + + + + +
    Table 13. /reviews/users/{userId}
    ParameterDescription

    userId

    평가 목록 조회 대상 유저의 아이디

    -

    응답

    +

    응답

    HTTP/1.1 200 OK
     Content-Type: application/json
     
    -{
    -  "reports" : [ {
    -    "id" : 1,
    -    "reporter" : {
    -      "id" : 1,
    -      "name" : "회원1"
    -    },
    -    "createdTime" : "2023-08-16T16:30:57",
    -    "auction" : {
    -      "id" : 1,
    -      "title" : "제목"
    -    },
    -    "description" : "신고합니다."
    -  }, {
    +[ {
    +  "id" : 3,
    +  "writer" : {
         "id" : 2,
    -    "reporter" : {
    -      "id" : 2,
    -      "name" : "회원2"
    -    },
    -    "createdTime" : "2023-08-16T16:30:57",
    -    "auction" : {
    -      "id" : 1,
    -      "title" : "제목"
    -    },
    -    "description" : "신고합니다."
    -  }, {
    -    "id" : 3,
    -    "reporter" : {
    -      "id" : 3,
    -      "name" : "회원3"
    -    },
    -    "createdTime" : "2023-08-16T16:30:57",
    -    "auction" : {
    -      "id" : 1,
    -      "title" : "제목"
    -    },
    -    "description" : "신고합니다."
    -  } ]
    -}
    + "name" : "판매자2", + "profileImage" : "http://localhost:8080/users/images/2" + }, + "content" : "친절하다.", + "score" : 5.0, + "createdTime" : "2023-10-09T11:03:03" +}, { + "id" : 2, + "writer" : { + "id" : 1, + "name" : "판매자1", + "profileImage" : "http://localhost:8080/users/images/1" + }, + "content" : "친절하다.", + "score" : 5.0, + "createdTime" : "2023-10-09T11:03:03" +} ]
    @@ -2691,173 +4586,108 @@

    응답

    - + - + - + - + - + - + - + - + - + - + - - - + + + - - - + + + - + - +

    reports.[]

    []

    Array

    모든 경매 신고 목록

    조회 대상 사용자가 받은 모든 평가 목록

    reports.[].id

    [].id

    Number

    경매 신고 글 ID

    사용자 평가 ID

    reports.[].reporter.id

    [].writer.id

    Number

    경매 신고한 사용자의 ID

    평가를 작성한 사용자의 ID

    reports.[].reporter.name

    [].writer.name

    String

    경매 신고한 사용자의 이름

    평가를 작성한 사용자의 이름

    reports.[].createdTime

    [].writer.profileImage

    String

    경매 신고 시간

    평가를 작성한 사용자의 프로필 이미지 url

    reports.[].auction.id

    Number

    신고한 경매 ID

    [].content

    String

    평가 내용

    reports.[].auction.title

    String

    신고한 경매 제목

    [].score

    Number

    평가 점수

    reports.[].description

    [].createdTime

    String

    신고 내용

    평가 작성 시간

    -

    채팅방 신고 등록

    +

    사용자가_경매_거래_상대에게_작성한_평가를_조회한다

    -

    요청

    +

    요청

    -
    POST /reports/chat-rooms HTTP/1.1
    +
    GET /auctions/1/reviews HTTP/1.1
     Content-Type: application/json
    -Authorization: Bearer accessToken
    -
    -{
    -  "chatRoomId" : 1,
    -  "description" : "신고합니다"
    -}
    +Authorization: Bearer accessToken
    + - + - - + +
    Table 14. /auctions/{auctionId}/reviews
    NameParameter Description

    Authorization

    회원 Bearer 인증 정보

    auctionId

    평가와_관련된_경매_아이디

    ---++ - - + - - - - - - - - + +
    PathTypeName Description

    chatRoomId

    Number

    신고할 채팅방 ID

    description

    String

    신고 내용

    Authorization

    회원 Bearer 인증 정보

    -

    응답

    -
    -
    -
    HTTP/1.1 201 Created
    -Location: /chattings/1
    -
    -
    -
    -
    -
    -

    채팅방 신고 조회

    -
    -

    요청

    -
    -
    -
    GET /reports/chat-rooms HTTP/1.1
    -Content-Type: application/json
    -
    -
    -
    -
    -

    응답

    +

    응답

    HTTP/1.1 200 OK
     Content-Type: application/json
     
     {
    -  "reports" : [ {
    -    "id" : 1,
    -    "reporter" : {
    -      "id" : 1,
    -      "name" : "회원1"
    -    },
    -    "createdTime" : "2023-08-16T16:30:57",
    -    "chatRoom" : {
    -      "id" : 1
    -    },
    -    "description" : "신고합니다."
    -  }, {
    -    "id" : 2,
    -    "reporter" : {
    -      "id" : 1,
    -      "name" : "회원1"
    -    },
    -    "createdTime" : "2023-08-16T16:30:57",
    -    "chatRoom" : {
    -      "id" : 1
    -    },
    -    "description" : "신고합니다."
    -  }, {
    -    "id" : 3,
    -    "reporter" : {
    -      "id" : 1,
    -      "name" : "회원1"
    -    },
    -    "createdTime" : "2023-08-16T16:30:57",
    -    "chatRoom" : {
    -      "id" : 1
    -    },
    -    "description" : "신고합니다."
    -  } ]
    +  "score" : 5.0,
    +  "content" : "친절하다."
     }
    @@ -2876,39 +4706,14 @@

    응답

    -

    reports.[]

    -

    Array

    -

    모든 채팅방 신고 목록

    - - -

    reports.[].id

    -

    Number

    -

    채팅방 신고 글 ID

    - - -

    reports.[].reporter.id

    -

    Number

    -

    채팅방을 신고한 사용자의 ID

    - - -

    reports.[].reporter.name

    -

    String

    -

    채팅방을 신고한 사용자의 이름

    - - -

    reports.[].createdTime

    -

    String

    -

    채팅방 신고 시간

    - - -

    reports.[].chatRoom.id

    +

    score

    Number

    -

    신고한 채팅방 ID

    +

    평가 점수

    -

    reports.[].description

    +

    content

    String

    -

    신고 내용

    +

    평가 내용

    @@ -2920,7 +4725,7 @@

    응답

    diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/application/AuctionServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/application/AuctionServiceTest.java index 1e0ea021f..0f2c62865 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/auction/application/AuctionServiceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/application/AuctionServiceTest.java @@ -1,46 +1,31 @@ package com.ddang.ddang.auction.application; -import com.ddang.ddang.auction.application.dto.CreateAuctionDto; import com.ddang.ddang.auction.application.dto.CreateInfoAuctionDto; import com.ddang.ddang.auction.application.dto.ReadAuctionDto; -import com.ddang.ddang.auction.application.dto.ReadAuctionWithChatRoomIdDto; import com.ddang.ddang.auction.application.dto.ReadAuctionsDto; import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; import com.ddang.ddang.auction.application.exception.UserForbiddenException; -import com.ddang.ddang.auction.domain.Auction; -import com.ddang.ddang.auction.domain.BidUnit; -import com.ddang.ddang.auction.domain.Price; -import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; -import com.ddang.ddang.authentication.domain.dto.AuthenticationUserInfo; -import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository; +import com.ddang.ddang.auction.application.fixture.AuctionServiceFixture; +import com.ddang.ddang.auction.presentation.dto.request.ReadAuctionSearchCondition; import com.ddang.ddang.category.application.exception.CategoryNotFoundException; -import com.ddang.ddang.category.domain.Category; -import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; -import com.ddang.ddang.chat.domain.ChatRoom; -import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; import com.ddang.ddang.configuration.IsolateDatabase; import com.ddang.ddang.image.domain.StoreImageProcessor; -import com.ddang.ddang.image.domain.dto.StoreImageDto; import com.ddang.ddang.region.application.exception.RegionNotFoundException; -import com.ddang.ddang.region.domain.Region; -import com.ddang.ddang.region.infrastructure.persistence.JpaRegionRepository; import com.ddang.ddang.user.application.exception.UserNotFoundException; -import com.ddang.ddang.user.domain.User; -import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.MediaType; -import org.springframework.mock.web.MockMultipartFile; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; -import java.time.LocalDateTime; import java.util.List; -import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; @@ -48,7 +33,7 @@ @IsolateDatabase @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class AuctionServiceTest { +class AuctionServiceTest extends AuctionServiceFixture { @MockBean StoreImageProcessor imageProcessor; @@ -56,984 +41,152 @@ class AuctionServiceTest { @Autowired AuctionService auctionService; - @Autowired - JpaAuctionRepository auctionRepository; - - @Autowired - JpaRegionRepository regionRepository; - - @Autowired - JpaCategoryRepository categoryRepository; - - @Autowired - JpaUserRepository userRepository; - - @Autowired - JpaChatRoomRepository chatRoomRepository; - - @Autowired - JpaBidRepository bidRepository; - @Test void 경매를_등록한다() { // given - final StoreImageDto storeImageDto = new StoreImageDto("upload.png", "store.png"); - - given(imageProcessor.storeImageFiles(any())).willReturn(List.of(storeImageDto)); - - final Region firstRegion = new Region("first"); - final Region secondRegion = new Region("second"); - final Region thirdRegion = new Region("third"); - - firstRegion.addSecondRegion(secondRegion); - secondRegion.addThirdRegion(thirdRegion); - - regionRepository.save(firstRegion); - - final Category main = new Category("main"); - final Category sub = new Category("sub"); - - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(seller); - - final MockMultipartFile auctionImage = new MockMultipartFile( - "image.png", - "image.png", - MediaType.IMAGE_PNG.toString(), - new byte[]{1}); - final CreateAuctionDto createAuctionDto = new CreateAuctionDto( - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - LocalDateTime.now(), - List.of(thirdRegion.getId()), - sub.getId(), - List.of(auctionImage), - 1L - ); + given(imageProcessor.storeImageFiles(any())).willReturn(List.of(경매_이미지_엔티티)); // when - final CreateInfoAuctionDto actual = auctionService.create(createAuctionDto); + final CreateInfoAuctionDto actual = auctionService.create(유효한_경매_생성_dto); // then assertThat(actual.id()).isPositive(); } @Test - void 지정한_아이디에_대한_회원이_없는_경우_경매를_등록하면_예외가_발생한다() { - // given - final StoreImageDto storeImageDto = new StoreImageDto("upload.png", "store.png"); - - given(imageProcessor.storeImageFiles(any())).willReturn(List.of(storeImageDto)); - - final Region firstRegion = new Region("first"); - final Region secondRegion = new Region("second"); - final Region thirdRegion = new Region("third"); - - firstRegion.addSecondRegion(secondRegion); - secondRegion.addThirdRegion(thirdRegion); - - regionRepository.save(firstRegion); - - final Category main = new Category("main"); - final Category sub = new Category("sub"); - - main.addSubCategory(sub); - categoryRepository.save(main); - - final MockMultipartFile auctionImage = new MockMultipartFile( - "image.png", - "image.png", - MediaType.IMAGE_PNG.toString(), - new byte[]{1} - ); - - final Long invalidSellerId = -999L; - - final CreateAuctionDto createAuctionDto = new CreateAuctionDto( - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - LocalDateTime.now(), - List.of(thirdRegion.getId()), - sub.getId(), - List.of(auctionImage), - invalidSellerId - ); - + void 지정한_아이디에_대한_판매자가_없는_경우_경매를_등록하면_예외가_발생한다() { // when & then - assertThatThrownBy(() -> auctionService.create(createAuctionDto)) + assertThatThrownBy(() -> auctionService.create(존재하지_않는_판매자의_경매_생성_dto)) .isInstanceOf(UserNotFoundException.class) .hasMessage("지정한 판매자를 찾을 수 없습니다."); } @Test void 지정한_아이디에_해당하는_지역이_없을때_경매를_등록하면_예외가_발생한다() { - // given - final Category main = new Category("main"); - final Category sub = new Category("sub"); - - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("789321") - .build(); - - userRepository.save(seller); - - final MockMultipartFile auctionImage = new MockMultipartFile( - "image.png", - "image.png", - MediaType.IMAGE_PNG.toString(), - new byte[]{1}); - final CreateAuctionDto createAuctionDto = new CreateAuctionDto( - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - LocalDateTime.now(), - List.of(3L), - sub.getId(), - List.of(auctionImage), - 1L - ); - // when & then - assertThatThrownBy(() -> auctionService.create(createAuctionDto)) + assertThatThrownBy(() -> auctionService.create(존재하지_않는_지역의_경매_생성_dto)) .isInstanceOf(RegionNotFoundException.class) - .hasMessage("지정한 세 번째 지역이 없거나 세 번째 지역이 아닙니다."); + .hasMessage("지정한 세 번째 지역이 없습니다."); } @Test void 지정한_아이디에_해당하는_지역이_세_번째_지역이_아닐_떄_경매를_등록하면_예외가_발생한다() { - // given - final Region firstRegion = new Region("first"); - final Region secondRegion = new Region("second"); - final Region thirdRegion = new Region("third"); - - firstRegion.addSecondRegion(secondRegion); - secondRegion.addThirdRegion(thirdRegion); - - regionRepository.save(firstRegion); - - final Category main = new Category("main"); - final Category sub = new Category("sub"); - - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("54321") - .build(); - - userRepository.save(seller); - - final MockMultipartFile auctionImage = new MockMultipartFile( - "image.png", - "image.png", - MediaType.IMAGE_PNG.toString(), - new byte[]{1}); - final CreateAuctionDto createAuctionDto = new CreateAuctionDto( - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - LocalDateTime.now(), - List.of(secondRegion.getId()), - sub.getId(), - List.of(auctionImage), - 1L - ); - // when & then - assertThatThrownBy(() -> auctionService.create(createAuctionDto)) + assertThatThrownBy(() -> auctionService.create(두_번째_지역의_경매_생성_dto)) .isInstanceOf(RegionNotFoundException.class) - .hasMessage("지정한 세 번째 지역이 없거나 세 번째 지역이 아닙니다."); + .hasMessage("지정한 세 번째 지역이 없습니다."); } @Test void 지정한_아이디에_해당하는_카테고리가_없을때_경매를_등록하면_예외가_발생한다() { - // given - final Region firstRegion = new Region("first"); - final Region secondRegion = new Region("second"); - final Region thirdRegion = new Region("third"); - - firstRegion.addSecondRegion(secondRegion); - secondRegion.addThirdRegion(thirdRegion); - - regionRepository.save(firstRegion); - - final User seller = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(seller); - - final MockMultipartFile auctionImage = new MockMultipartFile( - "image.png", - "image.png", - MediaType.IMAGE_PNG.toString(), - new byte[]{1}); - final CreateAuctionDto createAuctionDto = new CreateAuctionDto( - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - LocalDateTime.now(), - List.of(thirdRegion.getId()), - 1L, - List.of(auctionImage), - 1L - ); - // when & then - assertThatThrownBy(() -> auctionService.create(createAuctionDto)) + assertThatThrownBy(() -> auctionService.create(존재하지_않는_카테고리의_경매_생성_dto)) .isInstanceOf(CategoryNotFoundException.class) .hasMessage("지정한 하위 카테고리가 없거나 하위 카테고리가 아닙니다."); } @Test - void 지정한_아이디에_해당하는_카테고리가_하위_카테고리가_아닐_떄_경매를_등록하면_예외가_발생한다() { - // given - final Region firstRegion = new Region("first"); - final Region secondRegion = new Region("second"); - final Region thirdRegion = new Region("third"); - - firstRegion.addSecondRegion(secondRegion); - secondRegion.addThirdRegion(thirdRegion); - - regionRepository.save(firstRegion); - - final Category main = new Category("main"); - final Category sub = new Category("sub"); - - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(seller); - - final MockMultipartFile auctionImage = new MockMultipartFile( - "image.png", - "image.png", - MediaType.IMAGE_PNG.toString(), - new byte[]{1}); - final CreateAuctionDto createAuctionDto = new CreateAuctionDto( - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - LocalDateTime.now(), - List.of(thirdRegion.getId()), - main.getId(), - List.of(auctionImage), - seller.getId() - ); - + void 지정한_아이디에_해당하는_카테고리가_서브_카테고리가_아닐_떄_경매를_등록하면_예외가_발생한다() { // when & then - assertThatThrownBy(() -> auctionService.create(createAuctionDto)) + assertThatThrownBy(() -> auctionService.create(메인_카테고리의_경매_생성_dto)) .isInstanceOf(CategoryNotFoundException.class) .hasMessage("지정한 하위 카테고리가 없거나 하위 카테고리가 아닙니다."); } @Test - void 채팅방이_존재하고_채팅_자격이_있는_사용자가_지정한_아이디에_해당하는_경매를_조회하면_채팅방_아이디와_채팅_가능을_반환한다() { - // given - final StoreImageDto storeImageDto = new StoreImageDto("upload.png", "store.png"); - - given(imageProcessor.storeImageFiles(any())).willReturn(List.of(storeImageDto)); - - final Region firstRegion = new Region("first"); - final Region secondRegion = new Region("second"); - final Region thirdRegion = new Region("third"); - - firstRegion.addSecondRegion(secondRegion); - secondRegion.addThirdRegion(thirdRegion); - - regionRepository.save(firstRegion); - - final Category main = new Category("main"); - final Category sub = new Category("sub"); - - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User buyer = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - - userRepository.save(seller); - userRepository.save(buyer); - - final MockMultipartFile auctionImage = new MockMultipartFile( - "image.png", - "image.png", - MediaType.IMAGE_PNG.toString(), - new byte[]{1}); - final CreateAuctionDto createAuctionDto = new CreateAuctionDto( - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - LocalDateTime.now(), - List.of(thirdRegion.getId()), - sub.getId(), - List.of(auctionImage), - 1L - ); - - final Auction auction = createAuctionDto.toEntity(seller, sub); - auctionRepository.save(auction); - - final ChatRoom chatRoom = new ChatRoom(auction, buyer); - chatRoomRepository.save(chatRoom); - - // when - final ReadAuctionWithChatRoomIdDto actual = - auctionService.readByAuctionId(auction.getId(), new AuthenticationUserInfo(seller.getId())); - - // then - SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual.auctionDto().id()).isPositive(); - softAssertions.assertThat(actual.auctionDto().title()).isEqualTo(auction.getTitle()); - softAssertions.assertThat(actual.auctionDto().description()).isEqualTo(auction.getDescription()); - softAssertions.assertThat(actual.auctionDto().bidUnit()).isEqualTo(auction.getBidUnit().getValue()); - softAssertions.assertThat(actual.auctionDto().startPrice()).isEqualTo(auction.getStartPrice().getValue()); - softAssertions.assertThat(actual.auctionDto().lastBidPrice()).isNull(); - softAssertions.assertThat(actual.auctionDto().deleted()).isFalse(); - softAssertions.assertThat(actual.auctionDto().closingTime()).isEqualTo(auction.getClosingTime()); - softAssertions.assertThat(actual.auctionDto().auctioneerCount()).isEqualTo(0); - softAssertions.assertThat(actual.chatRoomDto().id()).isEqualTo(chatRoom.getId()); - softAssertions.assertThat(actual.chatRoomDto().isChatParticipant()).isTrue(); - }); - } - - @Test - void 채팅방이_존재하고_채팅_자격이_없는_사용자가_지정한_아이디에_해당하는_경매를_조회하면_채팅방_아이디와_채팅_불가를_반환한다() { - // given - final StoreImageDto storeImageDto = new StoreImageDto("upload.png", "store.png"); - - given(imageProcessor.storeImageFiles(any())).willReturn(List.of(storeImageDto)); - - final Region firstRegion = new Region("first"); - final Region secondRegion = new Region("second"); - final Region thirdRegion = new Region("third"); - - firstRegion.addSecondRegion(secondRegion); - secondRegion.addThirdRegion(thirdRegion); - - regionRepository.save(firstRegion); - - final Category main = new Category("main"); - final Category sub = new Category("sub"); - - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User buyer = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - final User stranger = User.builder() - .name("회원3") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12347") - .build(); - - userRepository.save(seller); - userRepository.save(buyer); - userRepository.save(stranger); - - final MockMultipartFile auctionImage = new MockMultipartFile( - "image.png", - "image.png", - MediaType.IMAGE_PNG.toString(), - new byte[]{1}); - final CreateAuctionDto createAuctionDto = new CreateAuctionDto( - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - LocalDateTime.now(), - List.of(thirdRegion.getId()), - sub.getId(), - List.of(auctionImage), - 1L - ); - - final Auction auction = createAuctionDto.toEntity(seller, sub); - auctionRepository.save(auction); - - final ChatRoom chatRoom = new ChatRoom(auction, buyer); - chatRoomRepository.save(chatRoom); - + void 지정한_아이디에_해당하는_경매를_조회한다() { // when - final ReadAuctionWithChatRoomIdDto actual = - auctionService.readByAuctionId(auction.getId(), new AuthenticationUserInfo(stranger.getId())); + final ReadAuctionDto actual = auctionService.readByAuctionId(구매자가_입찰한_경매1.getId()); // then - SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual.auctionDto().id()).isPositive(); - softAssertions.assertThat(actual.auctionDto().title()).isEqualTo(auction.getTitle()); - softAssertions.assertThat(actual.auctionDto().description()).isEqualTo(auction.getDescription()); - softAssertions.assertThat(actual.auctionDto().bidUnit()).isEqualTo(auction.getBidUnit().getValue()); - softAssertions.assertThat(actual.auctionDto().startPrice()).isEqualTo(auction.getStartPrice().getValue()); - softAssertions.assertThat(actual.auctionDto().lastBidPrice()).isNull(); - softAssertions.assertThat(actual.auctionDto().deleted()).isFalse(); - softAssertions.assertThat(actual.auctionDto().closingTime()).isEqualTo(auction.getClosingTime()); - softAssertions.assertThat(actual.auctionDto().auctioneerCount()).isEqualTo(0); - softAssertions.assertThat(actual.chatRoomDto().id()).isEqualTo(chatRoom.getId()); - softAssertions.assertThat(actual.chatRoomDto().isChatParticipant()).isFalse(); - }); - } - - @Test - void 경매가_종료되지_않은_상태에서_지정한_아이디에_해당하는_경매를_조회하면_채팅방_아이디_null과_채팅_불가를_반환한다() { - // given - final StoreImageDto storeImageDto = new StoreImageDto("upload.png", "store.png"); - - given(imageProcessor.storeImageFiles(any())).willReturn(List.of(storeImageDto)); - - final Region firstRegion = new Region("first"); - final Region secondRegion = new Region("second"); - final Region thirdRegion = new Region("third"); - - firstRegion.addSecondRegion(secondRegion); - secondRegion.addThirdRegion(thirdRegion); - - regionRepository.save(firstRegion); - - final Category main = new Category("main"); - final Category sub = new Category("sub"); - - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User buyer = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - - userRepository.save(seller); - userRepository.save(buyer); - - final MockMultipartFile auctionImage = new MockMultipartFile( - "image.png", - "image.png", - MediaType.IMAGE_PNG.toString(), - new byte[]{1}); - final CreateAuctionDto createAuctionDto = new CreateAuctionDto( - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - LocalDateTime.now().plusDays(3L), - List.of(thirdRegion.getId()), - sub.getId(), - List.of(auctionImage), - 1L - ); - - final Auction auction = createAuctionDto.toEntity(seller, sub); - auctionRepository.save(auction); - - // when - final ReadAuctionWithChatRoomIdDto actual = - auctionService.readByAuctionId(auction.getId(), new AuthenticationUserInfo(seller.getId())); - - // then - SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual.auctionDto().id()).isPositive(); - softAssertions.assertThat(actual.auctionDto().title()).isEqualTo(auction.getTitle()); - softAssertions.assertThat(actual.auctionDto().description()).isEqualTo(auction.getDescription()); - softAssertions.assertThat(actual.auctionDto().bidUnit()).isEqualTo(auction.getBidUnit().getValue()); - softAssertions.assertThat(actual.auctionDto().startPrice()).isEqualTo(auction.getStartPrice().getValue()); - softAssertions.assertThat(actual.auctionDto().lastBidPrice()).isNull(); - softAssertions.assertThat(actual.auctionDto().deleted()).isFalse(); - softAssertions.assertThat(actual.auctionDto().closingTime()).isEqualTo(auction.getClosingTime()); - softAssertions.assertThat(actual.auctionDto().auctioneerCount()).isEqualTo(0); - softAssertions.assertThat(actual.chatRoomDto().id()).isEqualTo(null); - softAssertions.assertThat(actual.chatRoomDto().isChatParticipant()).isFalse(); - }); - } - - @Test - void 경매가_종료되었지만_채팅방이_없는_상태에서_채팅_자격이_있는_사용자가_지정한_아이디에_해당하는_경매를_조회하면_채팅방_아이디_null과_채팅_가능을_반환한다() { - // given - final StoreImageDto storeImageDto = new StoreImageDto("upload.png", "store.png"); - - given(imageProcessor.storeImageFiles(any())).willReturn(List.of(storeImageDto)); - - final Region firstRegion = new Region("first"); - final Region secondRegion = new Region("second"); - final Region thirdRegion = new Region("third"); - - firstRegion.addSecondRegion(secondRegion); - secondRegion.addThirdRegion(thirdRegion); - - regionRepository.save(firstRegion); - - final Category main = new Category("main"); - final Category sub = new Category("sub"); - - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User buyer = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - - userRepository.save(seller); - userRepository.save(buyer); - - final MockMultipartFile auctionImage = new MockMultipartFile( - "image.png", - "image.png", - MediaType.IMAGE_PNG.toString(), - new byte[]{1}); - final CreateAuctionDto createAuctionDto = new CreateAuctionDto( - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - LocalDateTime.now(), - List.of(thirdRegion.getId()), - sub.getId(), - List.of(auctionImage), - 1L - ); - - final Auction auction = createAuctionDto.toEntity(seller, sub); - auctionRepository.save(auction); - - // when - final ReadAuctionWithChatRoomIdDto actual = - auctionService.readByAuctionId(auction.getId(), new AuthenticationUserInfo(seller.getId())); - - // then - SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual.auctionDto().id()).isPositive(); - softAssertions.assertThat(actual.auctionDto().title()).isEqualTo(auction.getTitle()); - softAssertions.assertThat(actual.auctionDto().description()).isEqualTo(auction.getDescription()); - softAssertions.assertThat(actual.auctionDto().bidUnit()).isEqualTo(auction.getBidUnit().getValue()); - softAssertions.assertThat(actual.auctionDto().startPrice()).isEqualTo(auction.getStartPrice().getValue()); - softAssertions.assertThat(actual.auctionDto().lastBidPrice()).isNull(); - softAssertions.assertThat(actual.auctionDto().deleted()).isFalse(); - softAssertions.assertThat(actual.auctionDto().closingTime()).isEqualTo(auction.getClosingTime()); - softAssertions.assertThat(actual.auctionDto().auctioneerCount()).isEqualTo(0); - softAssertions.assertThat(actual.chatRoomDto().id()).isEqualTo(null); - softAssertions.assertThat(actual.chatRoomDto().isChatParticipant()).isTrue(); - }); - } - - @Test - void 채팅방이_없고_채팅_자격이_없는_사용자가_지정한_아이디에_해당하는_경매를_조회하면_채팅방_아이디_null과_채팅_불가를_반환한다() { - // given - final StoreImageDto storeImageDto = new StoreImageDto("upload.png", "store.png"); - - given(imageProcessor.storeImageFiles(any())).willReturn(List.of(storeImageDto)); - - final Region firstRegion = new Region("first"); - final Region secondRegion = new Region("second"); - final Region thirdRegion = new Region("third"); - - firstRegion.addSecondRegion(secondRegion); - secondRegion.addThirdRegion(thirdRegion); - - regionRepository.save(firstRegion); - - final Category main = new Category("main"); - final Category sub = new Category("sub"); - - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User buyer = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - final User stranger = User.builder() - .name("회원3") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12347") - .build(); - - userRepository.save(seller); - userRepository.save(buyer); - userRepository.save(stranger); - - final MockMultipartFile auctionImage = new MockMultipartFile( - "image.png", - "image.png", - MediaType.IMAGE_PNG.toString(), - new byte[]{1}); - final CreateAuctionDto createAuctionDto = new CreateAuctionDto( - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - LocalDateTime.now(), - List.of(thirdRegion.getId()), - sub.getId(), - List.of(auctionImage), - 1L - ); - - final Auction auction = createAuctionDto.toEntity(seller, sub); - auctionRepository.save(auction); - - // when - final ReadAuctionWithChatRoomIdDto actual = - auctionService.readByAuctionId(auction.getId(), new AuthenticationUserInfo(stranger.getId())); - - // then - SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual.auctionDto().id()).isPositive(); - softAssertions.assertThat(actual.auctionDto().title()).isEqualTo(auction.getTitle()); - softAssertions.assertThat(actual.auctionDto().description()).isEqualTo(auction.getDescription()); - softAssertions.assertThat(actual.auctionDto().bidUnit()).isEqualTo(auction.getBidUnit().getValue()); - softAssertions.assertThat(actual.auctionDto().startPrice()).isEqualTo(auction.getStartPrice().getValue()); - softAssertions.assertThat(actual.auctionDto().lastBidPrice()).isNull(); - softAssertions.assertThat(actual.auctionDto().deleted()).isFalse(); - softAssertions.assertThat(actual.auctionDto().closingTime()).isEqualTo(auction.getClosingTime()); - softAssertions.assertThat(actual.auctionDto().auctioneerCount()).isEqualTo(0); - softAssertions.assertThat(actual.chatRoomDto().id()).isEqualTo(null); - softAssertions.assertThat(actual.chatRoomDto().isChatParticipant()).isFalse(); - }); + assertThat(actual.id()).isEqualTo(구매자가_입찰한_경매1.getId()); } @Test void 지정한_아이디에_해당하는_경매가_없는_경매를_조회시_예외가_발생한다() { - // given - final Long invalidAuctionId = -999L; - final AuthenticationUserInfo userInfo = new AuthenticationUserInfo(1L); - // when & then - assertThatThrownBy(() -> auctionService.readByAuctionId(invalidAuctionId, userInfo)) + assertThatThrownBy(() -> auctionService.readByAuctionId(존재하지_않는_경매_ID)) .isInstanceOf(AuctionNotFoundException.class) .hasMessage("지정한 아이디에 대한 경매를 찾을 수 없습니다."); } - @Test - void 요청을_한_회원의_정보를_찾을_수_없으면_예외가_발생한다() { - // given - final StoreImageDto storeImageDto = new StoreImageDto("upload.png", "store.png"); - - given(imageProcessor.storeImageFiles(any())).willReturn(List.of(storeImageDto)); - - final Region firstRegion = new Region("first"); - final Region secondRegion = new Region("second"); - final Region thirdRegion = new Region("third"); - - firstRegion.addSecondRegion(secondRegion); - secondRegion.addThirdRegion(thirdRegion); - - regionRepository.save(firstRegion); - - final Category main = new Category("main"); - final Category sub = new Category("sub"); - - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User buyer = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - - userRepository.save(seller); - userRepository.save(buyer); - - final MockMultipartFile auctionImage = new MockMultipartFile( - "image.png", - "image.png", - MediaType.IMAGE_PNG.toString(), - new byte[]{1}); - final CreateAuctionDto createAuctionDto = new CreateAuctionDto( - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - LocalDateTime.now(), - List.of(thirdRegion.getId()), - sub.getId(), - List.of(auctionImage), - 1L - ); - - final Auction auction = createAuctionDto.toEntity(seller, sub); - auctionRepository.save(auction); - - final Long auctionId = auction.getId(); - final Long invalidUserId = -999L; - AuthenticationUserInfo userInfo = new AuthenticationUserInfo(invalidUserId); - - // when & then - assertThatThrownBy(() -> auctionService.readByAuctionId(auctionId, userInfo)) - .isInstanceOf(UserNotFoundException.class) - .hasMessage("회원 정보를 찾을 수 없습니다."); - } - @Test void 첫번째_페이지의_경매_목록을_조회한다() { - // given - final StoreImageDto storeImageDto = new StoreImageDto("upload.png", "store.png"); - - given(imageProcessor.storeImageFiles(any())).willReturn(List.of(storeImageDto)); - - final Region firstRegion = new Region("first"); - final Region secondRegion = new Region("second"); - final Region thirdRegion = new Region("third"); - - firstRegion.addSecondRegion(secondRegion); - secondRegion.addThirdRegion(thirdRegion); - - regionRepository.save(firstRegion); - - final Category main = new Category("main"); - final Category sub = new Category("sub"); - - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(seller); - - final MockMultipartFile auctionImage = new MockMultipartFile( - "image.png", - "image.png", - MediaType.IMAGE_PNG.toString(), - new byte[]{1}); - final CreateAuctionDto createAuctionDto1 = new CreateAuctionDto( - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - LocalDateTime.now(), - List.of(thirdRegion.getId()), - sub.getId(), - List.of(auctionImage), - 1L - ); - final CreateAuctionDto createAuctionDto2 = new CreateAuctionDto( - "경매 상품 2", - "이것은 경매 상품 2 입니다.", - 1_000, - 1_000, - LocalDateTime.now(), - List.of(thirdRegion.getId()), - sub.getId(), - List.of(auctionImage), - 1L - ); - - auctionService.create(createAuctionDto1); - auctionService.create(createAuctionDto2); - // when - final ReadAuctionsDto actual = auctionService.readAllByLastAuctionId(null, 1); + final ReadAuctionsDto actual = auctionService.readAllByCondition( + PageRequest.of(0, 1, Sort.by(Order.desc("id"))), + new ReadAuctionSearchCondition(null) + ); // then SoftAssertions.assertSoftly(softAssertions -> { final List actualReadAuctionDtos = actual.readAuctionDtos(); softAssertions.assertThat(actualReadAuctionDtos).hasSize(1); - softAssertions.assertThat(actualReadAuctionDtos.get(0).title()).isEqualTo(createAuctionDto2.title()); + softAssertions.assertThat(actualReadAuctionDtos.get(0).title()).isEqualTo(구매자가_입찰한_경매2.getTitle()); }); } @Test void 지정한_아이디에_해당하는_경매를_삭제한다() { - // given - final User seller = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(seller); - - final Auction auction = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .seller(seller) - .build(); - - auctionRepository.save(auction); - - // when - auctionService.deleteByAuctionId(auction.getId(), seller.getId()); - - // then - final Optional actual = auctionRepository.findById(auction.getId()); - + // when & then SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual).isPresent(); - softAssertions.assertThat(actual.get().isDeleted()).isTrue(); + assertThatCode(() -> auctionService.deleteByAuctionId(종료되는_날이_3일_뒤인_경매.getId(), 판매자.getId())).doesNotThrowAnyException(); + assertThatThrownBy(() -> auctionService.readByAuctionId(종료되는_날이_3일_뒤인_경매.getId())) + .isInstanceOf(AuctionNotFoundException.class) + .hasMessage("지정한 아이디에 대한 경매를 찾을 수 없습니다."); }); } @Test void 지정한_아이디에_해당하는_경매가_없는_경매를_삭제시_예외가_발생한다() { - // given - final Long invalidAuctionId = -999L; - // when & then - assertThatThrownBy(() -> auctionService.deleteByAuctionId(invalidAuctionId, 1L)) + assertThatThrownBy(() -> auctionService.deleteByAuctionId(존재하지_않는_경매_ID, 판매자.getId())) .isInstanceOf(AuctionNotFoundException.class) .hasMessage("지정한 아이디에 대한 경매를 찾을 수 없습니다."); } @Test void 지정한_아이디에_해당하는_회원이_없는_경우_삭제시_예외가_발생한다() { - // given - final Auction auction = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final User seller = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(seller); - auctionRepository.save(auction); - final Long invalidSellerId = -999L; - final Long persistAuctionId = auction.getId(); - // when & then - assertThatThrownBy(() -> auctionService.deleteByAuctionId(persistAuctionId, invalidSellerId)) + assertThatThrownBy(() -> auctionService.deleteByAuctionId(종료되는_날이_3일_뒤인_경매.getId(), 존재하지_않는_사용자_ID)) .isInstanceOf(UserNotFoundException.class) .hasMessage("회원 정보를 찾을 수 없습니다."); } @Test void 지정한_아이디에_해당하는_회원과_판매자가_일치하지_않는_경우_삭제시_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> auctionService.deleteByAuctionId(구매자가_입찰한_경매2.getId(), 구매자.getId())) + .isInstanceOf(UserForbiddenException.class) + .hasMessage("권한이 없습니다."); + } + + @Test + void 회원이_등록한_경매_목록을_조회한다() { // given - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - userRepository.save(seller); - final Auction auction = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .seller(seller) - .build(); - final User user = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); + final PageRequest pageRequest = PageRequest.of(0, 3); - userRepository.save(user); - auctionRepository.save(auction); + // when + final ReadAuctionsDto actual = auctionService.readAllByUserId(판매자.getId(), pageRequest); - final Long persistAuctionId = auction.getId(); - final Long invalidSellerId = user.getId(); + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual.readAuctionDtos()).hasSize(3); + softAssertions.assertThat(actual.readAuctionDtos().get(0).id()).isEqualTo(종료된_경매.getId()); + softAssertions.assertThat(actual.readAuctionDtos().get(1).id()).isEqualTo(종료되는_날이_3일_뒤인_경매.getId()); + softAssertions.assertThat(actual.readAuctionDtos().get(2).id()).isEqualTo(구매자가_입찰한_경매2.getId()); + }); + } - // when & then - assertThatThrownBy(() -> auctionService.deleteByAuctionId(persistAuctionId, invalidSellerId)) - .isInstanceOf(UserForbiddenException.class) - .hasMessage("권한이 없습니다."); + @Test + void 회원이_참여한_경매_목록을_조회한다() { + final PageRequest pageRequest = PageRequest.of(0, 3); + final ReadAuctionsDto actual = auctionService.readAllByBidderId(구매자.getId(), pageRequest); + + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual.readAuctionDtos()).hasSize(2); + softAssertions.assertThat(actual.readAuctionDtos().get(0).id()).isEqualTo(구매자가_입찰한_경매2.getId()); + softAssertions.assertThat(actual.readAuctionDtos().get(1).id()).isEqualTo(구매자가_입찰한_경매1.getId()); + }); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/application/fixture/AuctionServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/application/fixture/AuctionServiceFixture.java new file mode 100644 index 000000000..271e86d44 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/application/fixture/AuctionServiceFixture.java @@ -0,0 +1,222 @@ +package com.ddang.ddang.auction.application.fixture; + +import com.ddang.ddang.auction.application.dto.CreateAuctionDto; +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.bid.domain.Bid; +import com.ddang.ddang.bid.domain.BidPrice; +import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.image.domain.dto.StoreImageDto; +import com.ddang.ddang.region.domain.Region; +import com.ddang.ddang.region.infrastructure.persistence.JpaRegionRepository; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class AuctionServiceFixture { + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaRegionRepository regionRepository; + + @Autowired + private JpaCategoryRepository categoryRepository; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaBidRepository bidRepository; + + private Category 가구_카테고리 = new Category("가구"); + private Category 가구_서브_의자_카테고리 = new Category("의자"); + + protected User 판매자 = User.builder() + .name("판매자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + protected User 구매자 = User.builder() + .name("구매자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("54321") + .build(); + + private MockMultipartFile 경매_이미지_파일 = new MockMultipartFile( + "image.png", + "image.png", + MediaType.IMAGE_PNG.toString(), + new byte[]{1} + ); + + private Long 존재하지_않는_지역_ID = -999L; + private Long 존재하지_않는_카테고리_ID = -999L; + private BidPrice 구매자가_입찰한_경매1_입찰_가격 = new BidPrice(1_000); + + protected Long 존재하지_않는_사용자_ID = -999L; + protected Long 존재하지_않는_경매_ID = -999L; + protected StoreImageDto 경매_이미지_엔티티 = new StoreImageDto("upload.png", "store.png"); + protected CreateAuctionDto 유효한_경매_생성_dto; + protected CreateAuctionDto 존재하지_않는_판매자의_경매_생성_dto; + protected CreateAuctionDto 존재하지_않는_지역의_경매_생성_dto; + protected CreateAuctionDto 두_번째_지역의_경매_생성_dto; + protected CreateAuctionDto 존재하지_않는_카테고리의_경매_생성_dto; + protected CreateAuctionDto 메인_카테고리의_경매_생성_dto; + protected CreateAuctionDto 종료되는_날이_3일_뒤인_경매_생성_dto; + protected CreateAuctionDto 종료된_경매_생성_dto; + protected Auction 종료되는_날이_3일_뒤인_경매; + protected Auction 구매자가_입찰한_경매1; + protected Auction 구매자가_입찰한_경매2; + protected Auction 종료된_경매; + private BidPrice 구매자가_입찰한_경매2_입찰_가격 = new BidPrice(10_000); + private Bid 구매자가_입찰한_경매1_입찰; + private Bid 구매자가_입찰한_경매2_입찰; + + @BeforeEach + void setUp() { + final Region 서울특별시 = new Region("서울특별시"); + final Region 강남구 = new Region("강남구"); + final Region 역삼동 = new Region("역삼동"); + + 서울특별시.addSecondRegion(강남구); + 강남구.addThirdRegion(역삼동); + + regionRepository.save(서울특별시); + + 가구_카테고리.addSubCategory(가구_서브_의자_카테고리); + + categoryRepository.save(가구_카테고리); + + userRepository.saveAll(List.of(판매자, 구매자)); + + 유효한_경매_생성_dto = new CreateAuctionDto( + "제목", + "내용", + 1_000, + 1_000, + LocalDateTime.now(), + List.of(역삼동.getId()), + 가구_서브_의자_카테고리.getId(), + List.of(경매_이미지_파일), + 판매자.getId() + ); + 종료된_경매_생성_dto = new CreateAuctionDto( + "제목", + "내용", + 1_000, + 1_000, + LocalDateTime.now().minusDays(3), + List.of(역삼동.getId()), + 가구_서브_의자_카테고리.getId(), + List.of(경매_이미지_파일), + 판매자.getId() + ); + 유효한_경매_생성_dto = new CreateAuctionDto( + "제목", + "내용", + 1_000, + 1_000, + LocalDateTime.now(), + List.of(역삼동.getId()), + 가구_서브_의자_카테고리.getId(), + List.of(경매_이미지_파일), + 판매자.getId() + ); + 존재하지_않는_판매자의_경매_생성_dto = new CreateAuctionDto( + "제목", + "내용", + 1_000, + 1_000, + LocalDateTime.now(), + List.of(역삼동.getId()), + 가구_서브_의자_카테고리.getId(), + List.of(경매_이미지_파일), + 존재하지_않는_사용자_ID + ); + 존재하지_않는_지역의_경매_생성_dto = new CreateAuctionDto( + "제목", + "내용", + 1_000, + 1_000, + LocalDateTime.now(), + List.of(존재하지_않는_지역_ID), + 가구_서브_의자_카테고리.getId(), + List.of(경매_이미지_파일), + 판매자.getId() + ); + 두_번째_지역의_경매_생성_dto = new CreateAuctionDto( + "제목", + "내용", + 1_000, + 1_000, + LocalDateTime.now(), + List.of(강남구.getId()), + 가구_서브_의자_카테고리.getId(), + List.of(경매_이미지_파일), + 판매자.getId() + ); + 존재하지_않는_카테고리의_경매_생성_dto = new CreateAuctionDto( + "제목", + "내용", + 1_000, + 1_000, + LocalDateTime.now(), + List.of(역삼동.getId()), + 존재하지_않는_카테고리_ID, + List.of(경매_이미지_파일), + 판매자.getId() + ); + 메인_카테고리의_경매_생성_dto = new CreateAuctionDto( + "제목", + "내용", + 1_000, + 1_000, + LocalDateTime.now(), + List.of(역삼동.getId()), + 가구_카테고리.getId(), + List.of(경매_이미지_파일), + 판매자.getId() + ); + 종료되는_날이_3일_뒤인_경매_생성_dto = new CreateAuctionDto( + "제목", + "내용", + 1_000, + 1_000, + LocalDateTime.now().plusDays(3), + List.of(역삼동.getId()), + 가구_서브_의자_카테고리.getId(), + List.of(경매_이미지_파일), + 판매자.getId() + ); + + 구매자가_입찰한_경매2 = 유효한_경매_생성_dto.toEntity(판매자, 가구_서브_의자_카테고리); + 종료되는_날이_3일_뒤인_경매 = 종료되는_날이_3일_뒤인_경매_생성_dto.toEntity(판매자, 가구_서브_의자_카테고리); + 구매자가_입찰한_경매1 = 유효한_경매_생성_dto.toEntity(판매자, 가구_서브_의자_카테고리); + 종료된_경매 = 유효한_경매_생성_dto.toEntity(판매자, 가구_서브_의자_카테고리); + + 구매자가_입찰한_경매1_입찰 = new Bid(구매자가_입찰한_경매1, 구매자, 구매자가_입찰한_경매1_입찰_가격); + 구매자가_입찰한_경매2_입찰 = new Bid(구매자가_입찰한_경매2, 구매자, 구매자가_입찰한_경매2_입찰_가격); + + bidRepository.saveAll(List.of(구매자가_입찰한_경매1_입찰, 구매자가_입찰한_경매2_입찰)); + + 구매자가_입찰한_경매1.updateLastBid(구매자가_입찰한_경매1_입찰); + 구매자가_입찰한_경매2.updateLastBid(구매자가_입찰한_경매2_입찰); + + auctionRepository.saveAll(List.of(구매자가_입찰한_경매1, 구매자가_입찰한_경매2, 종료되는_날이_3일_뒤인_경매, 종료된_경매)); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/configuration/util/SortParameterTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/configuration/util/SortParameterTest.java new file mode 100644 index 000000000..cb2e505c6 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/configuration/util/SortParameterTest.java @@ -0,0 +1,52 @@ +package com.ddang.ddang.auction.configuration.util; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ddang.ddang.auction.configuration.exception.UnsupportedSortParameterException; +import com.ddang.ddang.auction.configuration.util.fixture.SortParameterFixture; +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +@DataJpaTest +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +class SortParameterTest extends SortParameterFixture { + + @ParameterizedTest(name = "페이징 정렬 조건으로 {0}가 전달되면 {1}을 반환한다.") + @CsvSource(value = { + "new:id", "auctioneer:auctioneerCount", "closingTime:closingTime", "reliability:reliability" + }, delimiter = ':') + void 정렬_조건을_파라미터로_반환한다(final String sortParameter, final String expected) { + // when + final String actual = SortParameter.findSortProperty(sortParameter); + + // then + assertThat(actual).isEqualTo(expected); + } + + @Test + void 페이징_정렬_조건으로_null을_전달하면_기본_정렬_조건인_id를_반환한다() { + // when + final String actual = SortParameter.findSortProperty(null); + + // then + assertThat(actual).isEqualTo(AuctionSortConditionConsts.ID); + } + + @Test + void 지원하지_않는_정렬_조건이면_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> SortParameter.findSortProperty(지원하지_않는_정렬)) + .isInstanceOf(UnsupportedSortParameterException.class) + .hasMessage("지원하지 않는 정렬 방식입니다."); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/configuration/util/fixture/SortParameterFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/configuration/util/fixture/SortParameterFixture.java new file mode 100644 index 000000000..c128c4f86 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/configuration/util/fixture/SortParameterFixture.java @@ -0,0 +1,7 @@ +package com.ddang.ddang.auction.configuration.util.fixture; + +@SuppressWarnings("NonAsciiCharacters") +public class SortParameterFixture { + + protected String 지원하지_않는_정렬 = "invalid"; +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/AuctionTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/AuctionTest.java index 2f56b9cd0..e58fe81f8 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/AuctionTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/AuctionTest.java @@ -1,43 +1,27 @@ package com.ddang.ddang.auction.domain; +import static org.assertj.core.api.Assertions.assertThat; + +import com.ddang.ddang.auction.domain.fixture.AuctionFixture; import com.ddang.ddang.bid.domain.Bid; -import com.ddang.ddang.bid.domain.BidPrice; -import com.ddang.ddang.configuration.JpaConfiguration; -import com.ddang.ddang.configuration.QuerydslConfiguration; -import com.ddang.ddang.image.domain.AuctionImage; -import com.ddang.ddang.region.domain.AuctionRegion; -import com.ddang.ddang.region.domain.Region; import com.ddang.ddang.user.domain.User; -import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; -import org.springframework.test.util.ReflectionTestUtils; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; - -@DataJpaTest @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -@Import({JpaConfiguration.class, QuerydslConfiguration.class}) -class AuctionTest { - - @Autowired - JpaUserRepository userRepository; +class AuctionTest extends AuctionFixture { @Test void 경매를_삭제한다() { // given final Auction auction = Auction.builder() - .title("title") + .title("제목") .build(); // when @@ -51,38 +35,32 @@ class AuctionTest { void 경매에_직거래_지역_정보를_추가한다() { // given final Auction auction = Auction.builder() - .title("title") + .title("제목") .build(); - final Region firstRegion = new Region("서울특별시"); - final Region secondRegion = new Region("강남구"); - final Region thirdRegion = new Region("역삼동"); - - secondRegion.addThirdRegion(thirdRegion); - firstRegion.addSecondRegion(secondRegion); - - final AuctionRegion auctionRegion = new AuctionRegion(firstRegion); - // when - auction.addAuctionRegions(List.of(auctionRegion)); + auction.addAuctionRegions(List.of(서울특별시_강남구_역삼동)); // then - assertThat(auction.getAuctionRegions()).hasSize(1); + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(auction.getAuctionRegions()).hasSize(1); + softAssertions.assertThat(auction.getAuctionRegions()).contains(서울특별시_강남구_역삼동); + }); } @Test - void 첫_입찰자가_시작가_보다_낮은_금액으로_입찰하는_경우_참을_반환한다() { + void 첫_입찰자가_시작가_보다_낮은_금액으로_입찰하는_경우_유효하지_않다는_값으로_참을_반환한다() { // given final Auction auction = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") + .title("제목") + .description("설명") .bidUnit(new BidUnit(1_000)) .startPrice(new Price(1_000)) .closingTime(LocalDateTime.now().plusDays(7)) .build(); // when - final boolean actual = auction.isInvalidFirstBidPrice(new BidPrice(900)); + final boolean actual = auction.isInvalidFirstBidPrice(경매_시작가보다_적은_입찰_금액); // then assertThat(actual).isTrue(); @@ -92,17 +70,16 @@ class AuctionTest { void 경매_이미지_연관_관계를_세팅한다() { // given final Auction auction = Auction.builder() - .title("title") + .title("제목") .build(); - final AuctionImage auctionImage = new AuctionImage("image.png", "image.png"); // when - auction.addAuctionImages(List.of(auctionImage)); + auction.addAuctionImages(List.of(경매_이미지)); // then SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(auction.getAuctionImages()).isNotEmpty(); - softAssertions.assertThat(auctionImage.getAuction()).isNotNull(); + softAssertions.assertThat(auction.getAuctionImages()).contains(경매_이미지); + softAssertions.assertThat(경매_이미지.getAuction()).isEqualTo(auction); }); } @@ -110,7 +87,7 @@ class AuctionTest { void 경매가_특정_시간을_기준으로_종료되었는지_확인한다() { // given final Auction auction = Auction.builder() - .title("title") + .title("제목") .closingTime(LocalDateTime.now().minusDays(6)) .build(); @@ -125,59 +102,49 @@ class AuctionTest { void 경매_마지막_입찰_정보를_업데이트한다() { // given final Auction auction = Auction.builder() - .title("title") + .title("제목") .build(); - final User user = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - final Bid bid = new Bid(auction, user, new BidPrice(10_000)); + final Bid bid = new Bid(auction, 판매자, 유효한_입찰_금액); // when auction.updateLastBid(bid); // then - assertThat(auction.getLastBid()).isEqualTo(bid); - assertThat(auction.getAuctioneerCount()).isEqualTo(1); + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(auction.getLastBid()).isEqualTo(bid); + softAssertions.assertThat(bid.getAuction()).isEqualTo(auction); + softAssertions.assertThat(auction.getAuctioneerCount()).isEqualTo(1); + }); } @Test - void 특정_금액이_경매의_시작가보다_작다면_참을_반환한다() { + void 특정_금액이_경매의_시작가보다_작다면_유효하지_않다는_의미로_참을_반환한다() { // given final Auction auction = Auction.builder() - .title("title") + .title("제목") .startPrice(new Price(1_000)) .build(); // when - final boolean actual = auction.isInvalidFirstBidPrice(new BidPrice(900)); + final boolean actual = auction.isInvalidFirstBidPrice(경매_시작가보다_적은_입찰_금액); // then assertThat(actual).isTrue(); } @Test - void 특정_금액이_경매의_마지막_입찰가보다_작다면_참을_반환한다() { + void 특정_금액이_경매의_마지막_입찰가보다_작다면_마지막_입찰가가_크다는_의미로_참을_반환한다() { // given final Auction auction = Auction.builder() - .title("title") + .title("제목") .bidUnit(new BidUnit(1_000)) .build(); - final User user = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Bid bid = new Bid(auction, user, new BidPrice(10_000)); + final Bid bid = new Bid(auction, 판매자, 유효한_입찰_금액); auction.updateLastBid(bid); // when - final boolean actual = auction.isNextBidPriceGreaterThan(new BidPrice(9_000)); + final boolean actual = auction.isNextBidPriceGreaterThan(마지막_입찰가보다_적은_입찰_금액); // then assertThat(actual).isTrue(); @@ -186,20 +153,14 @@ class AuctionTest { @Test void 특정_회원이_경매_판매자와_일치하면_참을_반환한다() { // given - final User seller = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); final Auction auction = Auction.builder() - .title("title") + .title("제목") .bidUnit(new BidUnit(1_000)) - .seller(seller) + .seller(판매자) .build(); // when - final boolean actual = auction.isOwner(seller); + final boolean actual = auction.isOwner(판매자); // then assertThat(actual).isTrue(); @@ -208,28 +169,14 @@ class AuctionTest { @Test void 특정_회원이_경매_판매자와_일치하지_않으면_거짓을_반환한다() { // given - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); final Auction auction = Auction.builder() .title("title") .bidUnit(new BidUnit(1_000)) - .seller(seller) + .seller(판매자) .build(); - final User user = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - ReflectionTestUtils.setField(user, "id", 1L); // when - final boolean actual = auction.isOwner(user); + final boolean actual = auction.isOwner(구매자); // then assertThat(actual).isFalse(); @@ -238,33 +185,19 @@ class AuctionTest { @Test void 주어진_사용자가_판매자_또는_낙찰자라면_참을_반환한다() { // given - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User winner = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - - userRepository.save(seller); - userRepository.save(winner); - - final LocalDateTime pastTime = LocalDateTime.now().minusDays(3L); - final Auction auction = Auction.builder() - .title("경매") - .seller(seller) - .closingTime(pastTime) + .title("제목") + .seller(판매자) + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now().minusDays(3L)) .build(); - auction.updateLastBid(new Bid(auction, winner, new BidPrice(10_000))); + final Bid bid = new Bid(auction, 구매자, 유효한_입찰_금액); + + auction.updateLastBid(bid); // when - final boolean actual = auction.isSellerOrWinner(seller, LocalDateTime.now()); + final boolean actual = auction.isSellerOrWinner(구매자, LocalDateTime.now()); // then assertThat(actual).isTrue(); @@ -273,40 +206,17 @@ class AuctionTest { @Test void 주어진_사용자가_판매자_또는_낙찰자가_아니라면_거짓을_반환한다() { // given - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User winner = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - final User stranger = User.builder() - .name("회원3") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12347") - .build(); - - userRepository.save(seller); - userRepository.save(winner); - userRepository.save(stranger); - - final LocalDateTime pastTime = LocalDateTime.now().minusDays(3L); - final Auction auction = Auction.builder() - .title("경매") - .seller(seller) - .closingTime(pastTime) + .title("제목") + .seller(판매자) + .closingTime(LocalDateTime.now().minusDays(3L)) .build(); - auction.updateLastBid(new Bid(auction, winner, new BidPrice(10_000))); + final Bid bid = new Bid(auction, 구매자, 유효한_입찰_금액); + + auction.updateLastBid(bid); // when - final boolean actual = auction.isSellerOrWinner(stranger, LocalDateTime.now()); + final boolean actual = auction.isSellerOrWinner(사용자, LocalDateTime.now()); // then assertThat(actual).isFalse(); @@ -315,33 +225,17 @@ class AuctionTest { @Test void 주어진_사용자가_낙찰자라면_참을_반환한다() { // given - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User winner = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - - userRepository.save(seller); - userRepository.save(winner); - - final LocalDateTime pastTime = LocalDateTime.now().minusDays(3L); - final Auction auction = Auction.builder() - .title("경매") - .seller(seller) - .closingTime(pastTime) + .title("제목") + .seller(판매자) + .closingTime(LocalDateTime.now().minusDays(3L)) .build(); - auction.updateLastBid(new Bid(auction, winner, new BidPrice(10_000))); + final Bid bid = new Bid(auction, 구매자, 유효한_입찰_금액); + + auction.updateLastBid(bid); // when - final boolean actual = auction.isWinner(winner, LocalDateTime.now()); + final boolean actual = auction.isWinner(구매자, LocalDateTime.now()); // then assertThat(actual).isTrue(); @@ -350,40 +244,17 @@ class AuctionTest { @Test void 주어진_사용자가_낙찰자가_아니라면_거짓을_반환한다() { // given - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User winner = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - final User stranger = User.builder() - .name("회원3") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12347") - .build(); - - userRepository.save(seller); - userRepository.save(winner); - userRepository.save(stranger); - - final LocalDateTime pastTime = LocalDateTime.now().minusDays(3L); - final Auction auction = Auction.builder() - .title("경매") - .seller(seller) - .closingTime(pastTime) + .title("제목") + .seller(판매자) + .closingTime(LocalDateTime.now().minusDays(3L)) .build(); - auction.updateLastBid(new Bid(auction, winner, new BidPrice(10_000))); + final Bid bid = new Bid(auction, 구매자, 유효한_입찰_금액); + + auction.updateLastBid(bid); // when - final boolean actual = auction.isWinner(stranger, LocalDateTime.now()); + final boolean actual = auction.isWinner(사용자, LocalDateTime.now()); // then assertThat(actual).isFalse(); @@ -393,68 +264,33 @@ class AuctionTest { @Test void 경매의_최종_낙찰자를_반환한다() { // given - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User winner = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - - userRepository.save(seller); - userRepository.save(winner); - - final LocalDateTime pastTime = LocalDateTime.now().minusDays(3L); - final Auction auction = Auction.builder() - .title("경매") - .seller(seller) - .closingTime(pastTime) + .title("제목") + .seller(판매자) + .closingTime(LocalDateTime.now().minusDays(3L)) .build(); - auction.updateLastBid(new Bid(auction, winner, new BidPrice(10_000))); + final Bid bid = new Bid(auction, 구매자, 유효한_입찰_금액); + + auction.updateLastBid(bid); // when final Optional actual = auction.findWinner(LocalDateTime.now()); // then - SoftAssertions.assertSoftly(softAssertions -> { - assertThat(actual).isPresent(); - assertThat(actual).contains(winner); - }); + assertThat(actual).contains(구매자); } @Test - void 경매가_종료되지_않았다면_최종_낙찰자가_없다() { + void 경매가_종료되지_않았다면_최종_낙찰자는_없다() { // given - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User winner = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - - userRepository.save(seller); - userRepository.save(winner); - - final LocalDateTime futureTime = LocalDateTime.now().plusDays(3L); - final Auction auction = Auction.builder() - .title("경매") - .seller(seller) - .closingTime(futureTime) + .title("제목") + .seller(판매자) + .closingTime(LocalDateTime.now().plusDays(3L)) .build(); - auction.updateLastBid(new Bid(auction, winner, new BidPrice(10_000))); + final Bid bid = new Bid(auction, 구매자, 유효한_입찰_금액); + + auction.updateLastBid(bid); // when final Optional actual = auction.findWinner(LocalDateTime.now()); @@ -466,26 +302,122 @@ class AuctionTest { @Test void 입찰자가_존재하지_않는다면_최종_낙찰자가_없다() { // given - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - userRepository.save(seller); + final Auction auction = Auction.builder() + .title("제목") + .seller(판매자) + .closingTime(LocalDateTime.now().minusDays(3L)) + .build(); - final LocalDateTime pastTime = LocalDateTime.now().minusDays(3L); + // when + final Optional actual = auction.findWinner(LocalDateTime.now()); + // then + assertThat(actual).isEmpty(); + } + + @Test + void 마지막_입찰자를_반환한다() { + // given final Auction auction = Auction.builder() - .title("경매") - .seller(seller) - .closingTime(pastTime) + .title("제목") + .seller(판매자) + .closingTime(LocalDateTime.now().minusDays(3L)) + .startPrice(new Price(1_000)) + .bidUnit(new BidUnit(1_000)) .build(); + final Bid bid = new Bid(auction, 구매자, 유효한_입찰_금액); + + auction.updateLastBid(bid); // when - final Optional actual = auction.findWinner(LocalDateTime.now()); + final Optional actual = auction.findLastBidder(); + + // then + assertThat(actual).contains(구매자); + } + + @Test + void 마지막_입찰자가_없다면_빈_Optional을_반환한다() { + // given + final Auction auction = Auction.builder() + .title("제목") + .seller(판매자) + .closingTime(LocalDateTime.now().minusDays(3L)) + .build(); + + // when + final Optional actual = auction.findLastBidder(); // then assertThat(actual).isEmpty(); } + + @Test + void 경매를_진행중이며_입찰자가_없는_경우_UNBIDDEN을_반환한다() { + // given + final Auction auction = Auction.builder() + .title("제목") + .seller(판매자) + .closingTime(LocalDateTime.now().plusDays(2)) + .build(); + + // when + final AuctionStatus actual = auction.findAuctionStatus(LocalDateTime.now()); + + // then + assertThat(actual).isEqualTo(AuctionStatus.UNBIDDEN); + } + + @Test + void 경매가_마감되었고_입찰자가_없는_경우_FAILURE를_반환한다() { + // given + final Auction auction = Auction.builder() + .title("제목") + .seller(판매자) + .closingTime(LocalDateTime.now().minusDays(2)) + .build(); + + // when + final AuctionStatus actual = auction.findAuctionStatus(LocalDateTime.now()); + + assertThat(actual).isEqualTo(AuctionStatus.FAILURE); + } + + @Test + void 경매가_진행중이며_입찰자가_있는_경우_ONGOING을_반환한다() { + // given + final Auction auction = Auction.builder() + .title("제목") + .seller(판매자) + .closingTime(LocalDateTime.now().plusDays(2)) + .build(); + final Bid bid = new Bid(auction, 구매자, 유효한_입찰_금액); + + auction.updateLastBid(bid); + + // when + final AuctionStatus actual = auction.findAuctionStatus(LocalDateTime.now()); + + // then + assertThat(actual).isEqualTo(AuctionStatus.ONGOING); + } + + @Test + void 경매가_마감되었고_입찰자가_있는_경우_SUCCESS를_반환한다() { + // given + final Auction auction = Auction.builder() + .title("제목") + .seller(판매자) + .closingTime(LocalDateTime.now().minusDays(2)) + .build(); + final Bid bid = new Bid(auction, 구매자, 유효한_입찰_금액); + + auction.updateLastBid(bid); + + // when + final AuctionStatus actual = auction.findAuctionStatus(LocalDateTime.now()); + + // then + assertThat(actual).isEqualTo(AuctionStatus.SUCCESS); + } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/BidUnitTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/BidUnitTest.java index ee2f5f361..56e03a537 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/BidUnitTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/BidUnitTest.java @@ -1,16 +1,17 @@ package com.ddang.ddang.auction.domain; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + import com.ddang.ddang.auction.domain.exception.InvalidPriceValueException; +import com.ddang.ddang.auction.domain.fixture.BidUnitFixture; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; - @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class BidUnitTest { +class BidUnitTest extends BidUnitFixture { @Test void 가능한_범위_내의_가격을_받는_경우_정상적으로_생성된다() { @@ -20,33 +21,24 @@ class BidUnitTest { @Test void 최소_가격_미만으로_받는_경우_예외가_발생한다() { - // given - final int invalidBidUnit = -1; - // when & then - assertThatThrownBy(() -> new BidUnit(invalidBidUnit)) + assertThatThrownBy(() -> new BidUnit(최소_금액보다_낮은_입찰_단위)) .isInstanceOf(InvalidPriceValueException.class) .hasMessage("입찰 단위는 0원 이상이어야 합니다."); } @Test void 최대_가격_초과하여_받는_경우_예외가_발생한다() { - // given - final int invalidBidUnit = Integer.MAX_VALUE; - // when & then - assertThatThrownBy(() -> new BidUnit(invalidBidUnit)) + assertThatThrownBy(() -> new BidUnit(최대_금액보다_높은_입찰_단위)) .isInstanceOf(InvalidPriceValueException.class) .hasMessage("입찰 단위는 2100000000원 이하여야 합니다."); } @Test void 유효하지_않은_가격_단위를_받는_경우_예외가_발생한다() { - // given - final int invalidBidUnit = 1_001; - // when & then - assertThatThrownBy(() -> new BidUnit(invalidBidUnit)) + assertThatThrownBy(() -> new BidUnit(단위가_100이_아닌_입찰_단위)) .isInstanceOf(InvalidPriceValueException.class) .hasMessage("입찰 단위는 100의 배수여야 합니다."); } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/PriceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/PriceTest.java index 7236c7800..1a2f2b13a 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/PriceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/PriceTest.java @@ -1,16 +1,17 @@ package com.ddang.ddang.auction.domain; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + import com.ddang.ddang.auction.domain.exception.InvalidPriceValueException; +import com.ddang.ddang.auction.domain.fixture.PriceFixture; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; - @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class PriceTest { +class PriceTest extends PriceFixture { @Test void 가능한_범위_내의_가격을_받는_경우_정상적으로_생성된다() { @@ -20,22 +21,16 @@ class PriceTest { @Test void 최소_가격_미만으로_받는_경우_예외가_발생한다() { - // given - final int invalidPrice = -1; - // when & then - assertThatThrownBy(() -> new Price(invalidPrice)) + assertThatThrownBy(() -> new Price(최소_금액보다_낮은_가격)) .isInstanceOf(InvalidPriceValueException.class) .hasMessage("가격은 0원 이상이어야 합니다."); } @Test void 최대_가격_초과하여_받는_경우_예외가_발생한다() { - // given - final int invalidPrice = Integer.MAX_VALUE; - // when & then - assertThatThrownBy(() -> new Price(invalidPrice)) + assertThatThrownBy(() -> new Price(최대_금액보다_높은_가격)) .isInstanceOf(InvalidPriceValueException.class) .hasMessage("가격은 2100000000원 이하여야 합니다."); } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/fixture/AuctionFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/fixture/AuctionFixture.java new file mode 100644 index 000000000..752c156ff --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/fixture/AuctionFixture.java @@ -0,0 +1,56 @@ +package com.ddang.ddang.auction.domain.fixture; + +import com.ddang.ddang.bid.domain.BidPrice; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.region.domain.AuctionRegion; +import com.ddang.ddang.region.domain.Region; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.test.util.ReflectionTestUtils; + +@SuppressWarnings("NonAsciiCharacters") +public class AuctionFixture { + + protected AuctionRegion 서울특별시_강남구_역삼동; + protected BidPrice 경매_시작가보다_적은_입찰_금액 = new BidPrice(900); + protected BidPrice 유효한_입찰_금액 = new BidPrice(10_000); + protected BidPrice 마지막_입찰가보다_적은_입찰_금액 = new BidPrice(5_000); + protected AuctionImage 경매_이미지; + protected User 판매자 = User.builder() + .name("판매자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + protected User 구매자 = User.builder() + .name("구매자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("54321") + .build(); + protected User 사용자 = User.builder() + .name("사용자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("98765") + .build(); + + @BeforeEach + void setUp() { + final Region 서울특별시 = new Region("서울특별시"); + final Region 강남구 = new Region("강남구"); + final Region 역삼동 = new Region("역삼동"); + 강남구.addThirdRegion(역삼동); + 서울특별시.addSecondRegion(강남구); + + 서울특별시_강남구_역삼동 = new AuctionRegion(서울특별시); + + 경매_이미지 = new AuctionImage("image.png", "image.png"); + ReflectionTestUtils.setField(경매_이미지, "id", 1L); + ReflectionTestUtils.setField(판매자, "id", 1L); + ReflectionTestUtils.setField(구매자, "id", 2L); + ReflectionTestUtils.setField(사용자, "id", 3L); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/fixture/BidUnitFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/fixture/BidUnitFixture.java new file mode 100644 index 000000000..cb1da4453 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/fixture/BidUnitFixture.java @@ -0,0 +1,9 @@ +package com.ddang.ddang.auction.domain.fixture; + +@SuppressWarnings("NonAsciiCharacters") +public class BidUnitFixture { + + protected int 최소_금액보다_낮은_입찰_단위 = -1; + protected int 최대_금액보다_높은_입찰_단위 = Integer.MAX_VALUE; + protected int 단위가_100이_아닌_입찰_단위 = 1_001; +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/fixture/PriceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/fixture/PriceFixture.java new file mode 100644 index 000000000..e07bc685c --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/fixture/PriceFixture.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.auction.domain.fixture; + +@SuppressWarnings("NonAsciiCharacters") +public class PriceFixture { + + protected int 최소_금액보다_낮은_가격 = -1; + protected int 최대_금액보다_높은_가격 = Integer.MAX_VALUE; +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListByBidderIdTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListByBidderIdTest.java new file mode 100644 index 000000000..9ef4bdf99 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListByBidderIdTest.java @@ -0,0 +1,124 @@ +package com.ddang.ddang.auction.infrastructure.persistence; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.infrastructure.persistence.fixture.AuctionForListByBidderIdFixture; +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AuctionForListByBidderIdTest extends AuctionForListByBidderIdFixture { + + QuerydslAuctionRepository querydslAuctionRepository; + + @BeforeEach + void setUp(@Autowired final JPAQueryFactory queryFactory) { + querydslAuctionRepository = new QuerydslAuctionRepositoryImpl(queryFactory); + } + + @Nested + class 참여한_경매가_7개인_사용자_테스트 { + + @Test + void 페이지_크기_3_첫번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByBidderId( + 참여한_경매가_7개인_사용자.getId(), + PageRequest.of(0, 페이지_크기_3) + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(첫번째_페이지_인덱스_0); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(첫번째_페이지_인덱스_1); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(첫번째_페이지_인덱스_2); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_두번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByBidderId( + 참여한_경매가_7개인_사용자.getId(), + PageRequest.of(1, 페이지_크기_3) + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(두번째_페이지_인덱스_0); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(두번째_페이지_인덱스_1); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(두번째_페이지_인덱스_2); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_세번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByBidderId( + 참여한_경매가_7개인_사용자.getId(), + PageRequest.of(2, 페이지_크기_3) + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(1); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(세번째_페이지_인덱스_0); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } + + @Test + void 페이지_크기_3_네번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByBidderId( + 참여한_경매가_7개인_사용자.getId(), + PageRequest.of(3, 페이지_크기_3) + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isEmpty(); + softAssertions.assertThat(actual.getContent()).isEmpty(); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } + } + + @Nested + class 참여한_경매가_없는_사용자_테스트 { + + @Test + void 페이지_크기_3_첫번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByBidderId( + 참여한_경매가_없는_사용자.getId(), + PageRequest.of(0, 페이지_크기_3) + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isEmpty(); + softAssertions.assertThat(actual.getContent()).isEmpty(); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListByUserIdTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListByUserIdTest.java new file mode 100644 index 000000000..5c200dc28 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListByUserIdTest.java @@ -0,0 +1,107 @@ +package com.ddang.ddang.auction.infrastructure.persistence; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.infrastructure.persistence.fixture.AuctionForListByUserIdFixture; +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AuctionForListByUserIdTest extends AuctionForListByUserIdFixture { + + QuerydslAuctionRepository querydslAuctionRepository; + + @BeforeEach + void setUp(@Autowired final JPAQueryFactory queryFactory) { + querydslAuctionRepository = new QuerydslAuctionRepositoryImpl(queryFactory); + } + + @Nested + class 등록한_경매가_5개인_사용자_테스트 { + + @Test + void 첫번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByUserId( + 등록한_경매가_5개인_사용자.getId(), + PageRequest.of(0, 페이지_크기_3) + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(첫번째_페이지_인덱스_0); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(첫번째_페이지_인덱스_1); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(첫번째_페이지_인덱스_2); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 두번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByUserId( + 등록한_경매가_5개인_사용자.getId(), + PageRequest.of(1, 페이지_크기_3) + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(2); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(두번째_페이지_인덱스_0); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(두번째_페이지_인덱스_1); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } + + @Test + void 세번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByUserId( + 등록한_경매가_5개인_사용자.getId(), + PageRequest.of(2, 페이지_크기_3) + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isEmpty(); + softAssertions.assertThat(actual.getContent()).isEmpty(); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } + } + + @Nested + class 등록한_경매가_없는_사용자_테스트 { + + @Test + void 페이지_크기_3_첫번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByUserId( + 등록한_경매가_없는_사용자.getId(), + PageRequest.of(0, 페이지_크기_3) + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isEmpty(); + softAssertions.assertThat(actual.getContent()).isEmpty(); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSearchByTitleAndSortByAuctioneerCountTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSearchByTitleAndSortByAuctioneerCountTest.java new file mode 100644 index 000000000..ebf8de098 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSearchByTitleAndSortByAuctioneerCountTest.java @@ -0,0 +1,135 @@ +package com.ddang.ddang.auction.infrastructure.persistence; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.infrastructure.persistence.fixture.AuctionForListSearchByTitleAndSortByAuctioneerCountFixture; +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AuctionForListSearchByTitleAndSortByAuctioneerCountTest extends AuctionForListSearchByTitleAndSortByAuctioneerCountFixture { + + QuerydslAuctionRepository querydslAuctionRepository; + + @BeforeEach + void setUp(@Autowired final JPAQueryFactory queryFactory) { + querydslAuctionRepository = new QuerydslAuctionRepositoryImpl(queryFactory); + } + + @Test + void 첫번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(0, 페이지_크기_3, 참여_인원순_정렬), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(첫번째_페이지_인덱스_0_참여자_7_2일_후_마감_id_4); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(첫번째_페이지_인덱스_1_참여자_6_4일_후_마감_id_15); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(첫번째_페이지_인덱스_2_참여자_6_4일_후_마감_id_14); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 두번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(1, 페이지_크기_3, 참여_인원순_정렬), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(두번째_페이지_인덱스_0_참여자_6_4일_후_마감_id_12); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(두번째_페이지_인덱스_1_참여자_6_4일_후_마감_id_11); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(두번째_페이지_인덱스_2_참여자_6_4일_후_마감_id_10); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 세번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(2, 페이지_크기_3, 참여_인원순_정렬), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(세번째_페이지_인덱스_0_참여자_4_3일_후_마감_id_3); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(세번째_페이지_인덱스_1_참여자_1_4일_후_마감_id_2); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(세번째_페이지_인덱스_2_참여자_6_4일_전_마감_id_16); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 네번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(3, 페이지_크기_3, 참여_인원순_정렬), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(네번째_페이지_인덱스_0_참여자_6_4일_전_마감_id_13); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(네번째_페이지_인덱스_1_참여자_6_4일_전_마감_id_9); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(네번째_페이지_인덱스_2_참여자_6_4일_전_마감_id_8); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 다섯번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(4, 페이지_크기_3, 참여_인원순_정렬), + 검색어_맥북 + ); + + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(2); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(다섯번째_페이지_인덱스_0_참여자_3_4일_전_마감_id_7); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(다섯번째_페이지_인덱스_1_참여자_2_5일_전_마감_id_1); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } + + @Test + void 페이지_크기_3_여섯번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(5, 페이지_크기_3, 참여_인원순_정렬), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isEmpty(); + softAssertions.assertThat(actual.getContent()).isEmpty(); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSearchByTitleAndSortByClosingTimeTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSearchByTitleAndSortByClosingTimeTest.java new file mode 100644 index 000000000..5c89f96a4 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSearchByTitleAndSortByClosingTimeTest.java @@ -0,0 +1,135 @@ +package com.ddang.ddang.auction.infrastructure.persistence; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.infrastructure.persistence.fixture.AuctionForListSearchByTitleAndSortByClosingTimeFixture; +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AuctionForListSearchByTitleAndSortByClosingTimeTest extends AuctionForListSearchByTitleAndSortByClosingTimeFixture { + + QuerydslAuctionRepository querydslAuctionRepository; + + @BeforeEach + void setUp(@Autowired final JPAQueryFactory queryFactory) { + querydslAuctionRepository = new QuerydslAuctionRepositoryImpl(queryFactory); + } + + @Test + void 첫번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(0, 페이지_크기_3, 마감_임박순_정렬), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(첫번째_페이지_인덱스_0_2일_후_마감_id_4); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(첫번째_페이지_인덱스_1_3일_후_마감_id_3); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(첫번째_페이지_인덱스_2_4일_후_마감_id_15); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 두번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(1, 페이지_크기_3, 마감_임박순_정렬), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(두번째_페이지_인덱스_0_4일_후_마감_id_14); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(두번째_페이지_인덱스_1_4일_후_마감_id_12); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(두번째_페이지_인덱스_2_4일_후_마감_id_11); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 세번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(2, 페이지_크기_3, 마감_임박순_정렬), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(세번째_페이지_인덱스_0_4일_후_마감_id_10); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(세번째_페이지_인덱스_1_4일_후_마감_id_2); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(세번째_페이지_인덱스_2_5일_전_마감_id_1); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 네번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(3, 페이지_크기_3, 마감_임박순_정렬), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(네번째_페이지_인덱스_0_4일_전_마감_id_16); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(네번째_페이지_인덱스_1_4일_전_마감_id_13); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(네번째_페이지_인덱스_2_4일_전_마감_id_9); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 다섯번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(4, 페이지_크기_3, 마감_임박순_정렬), + 검색어_맥북 + ); + + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(2); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(다섯번째_페이지_인덱스_0_4일_전_마감_id_8); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(다섯번째_페이지_인덱스_1_4일_전_마감_id_7); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } + + @Test + void 페이지_크기_3_여섯번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(5, 페이지_크기_3, 마감_임박순_정렬), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isEmpty(); + softAssertions.assertThat(actual.getContent()).isEmpty(); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSearchByTitleAndSortByIdTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSearchByTitleAndSortByIdTest.java new file mode 100644 index 000000000..cbc0926b8 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSearchByTitleAndSortByIdTest.java @@ -0,0 +1,135 @@ +package com.ddang.ddang.auction.infrastructure.persistence; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.infrastructure.persistence.fixture.AuctionForListSearchByTitleAndSortByIdFixture; +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AuctionForListSearchByTitleAndSortByIdTest extends AuctionForListSearchByTitleAndSortByIdFixture { + + QuerydslAuctionRepository querydslAuctionRepository; + + @BeforeEach + void setUp(@Autowired final JPAQueryFactory queryFactory) { + querydslAuctionRepository = new QuerydslAuctionRepositoryImpl(queryFactory); + } + + @Test + void 첫번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(0, 페이지_크기_3, id순_정렬), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(첫번째_페이지_인덱스_0_id_16); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(첫번째_페이지_인덱스_1_id_15); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(첫번째_페이지_인덱스_2_id_14); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 두번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(1, 페이지_크기_3, id순_정렬), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(두번째_페이지_인덱스_0_id_13); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(두번째_페이지_인덱스_1_id_12); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(두번째_페이지_인덱스_2_id_11); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 세번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(2, 페이지_크기_3, id순_정렬), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(세번째_페이지_인덱스_0_id_10); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(세번째_페이지_인덱스_1_id_9); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(세번째_페이지_인덱스_2_id_8); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 네번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(3, 페이지_크기_3, id순_정렬), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(네번째_페이지_인덱스_0_id_7); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(네번째_페이지_인덱스_1_id_4); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(네번째_페이지_인덱스_2_id_3); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 다섯번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(4, 페이지_크기_3, id순_정렬), + 검색어_맥북 + ); + + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(2); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(다섯번째_페이지_인덱스_0_id_2); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(다섯번째_페이지_인덱스_1_id_1); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } + + @Test + void 페이지_크기_3_여섯번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(5, 페이지_크기_3, id순_정렬), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isEmpty(); + softAssertions.assertThat(actual.getContent()).isEmpty(); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSearchByTitleAndSortByReliabilityTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSearchByTitleAndSortByReliabilityTest.java new file mode 100644 index 000000000..573a53bd4 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSearchByTitleAndSortByReliabilityTest.java @@ -0,0 +1,135 @@ +package com.ddang.ddang.auction.infrastructure.persistence; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.infrastructure.persistence.fixture.AuctionForListSearchByTitleAndSortByReliabilityFixture; +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AuctionForListSearchByTitleAndSortByReliabilityTest extends AuctionForListSearchByTitleAndSortByReliabilityFixture { + + QuerydslAuctionRepository querydslAuctionRepository; + + @BeforeEach + void setUp(@Autowired final JPAQueryFactory queryFactory) { + querydslAuctionRepository = new QuerydslAuctionRepositoryImpl(queryFactory); + } + + @Test + void 페이지_크기_3_첫번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(0, 페이지_크기_3, 신뢰도순_정렬), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(첫번째_페이지_인덱스_0_신뢰도_5_0_2일_후_마감_id_4); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(첫번째_페이지_인덱스_1_신뢰도_3_5_4일_후_마감_id_14); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(첫번째_페이지_인덱스_2_신뢰도_3_5_4일_후_마감_id_12); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_두번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(1, 페이지_크기_3, 신뢰도순_정렬), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(두번째_페이지_인덱스_0_신뢰도_3_5_4일_후_마감_id_10); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(두번째_페이지_인덱스_1_신뢰도_2_1_4일_후_마감_id_15); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(두번째_페이지_인덱스_2_신뢰도_2_1_4일_후_마감_id_11); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_세번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(2, 페이지_크기_3, 신뢰도순_정렬), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(세번째_페이지_인덱스_0_신뢰도_2_1_3일_후_마감_id_3); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(세번째_페이지_인덱스_1_신뢰도_2_1_4일_후_마감_id_2); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(세번째_페이지_인덱스_2_신뢰도_4_7_4일_전_마감_id_7); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_네번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(3, 페이지_크기_3, 신뢰도순_정렬), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(네번째_페이지_인덱스_0_신뢰도_4_7_5일_전_마감_id_1); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(네번째_페이지_인덱스_1_신뢰도_3_5_4일_전_마감_id_16); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(네번째_페이지_인덱스_2_신뢰도_3_5_4일_전_마감_id_8); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_다섯번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(4, 페이지_크기_3, 신뢰도순_정렬), + 검색어_맥북 + ); + + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(2); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(다섯번째_페이지_인덱스_0_신뢰도_2_1_4일_전_마감_id_13); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(다섯번째_페이지_인덱스_1_신뢰도_2_1_4일_전_마감_id_9); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } + + @Test + void 페이지_크기_3_여섯번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(5, 페이지_크기_3, 신뢰도순_정렬), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isEmpty(); + softAssertions.assertThat(actual.getContent()).isEmpty(); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSearchByTitleTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSearchByTitleTest.java new file mode 100644 index 000000000..0364d7c8d --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSearchByTitleTest.java @@ -0,0 +1,161 @@ +package com.ddang.ddang.auction.infrastructure.persistence; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.infrastructure.persistence.fixture.AuctionForListSearchByTitleFixture; +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AuctionForListSearchByTitleTest extends AuctionForListSearchByTitleFixture { + + QuerydslAuctionRepository querydslAuctionRepository; + + @BeforeEach + void setUp(@Autowired final JPAQueryFactory queryFactory) { + querydslAuctionRepository = new QuerydslAuctionRepositoryImpl(queryFactory); + } + + @Nested + class 검색_결과가_14개인_검색어_테스트 { + + @Test + void 페이지_크기_3_첫번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(0, 페이지_크기_3), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(첫번째_페이지_인덱스_0_맥북_검색_id_16); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(첫번째_페이지_인덱스_1_맥북_검색_id_15); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(첫번째_페이지_인덱스_2_맥북_검색_id_14); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_두번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(1, 페이지_크기_3), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(두번째_페이지_인덱스_0_맥북_검색_id_13); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(두번째_페이지_인덱스_1_맥북_검색_id_12); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(두번째_페이지_인덱스_2_맥북_검색_id_11); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_세번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(2, 페이지_크기_3), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(세번째_페이지_인덱스_0_맥북_검색_id_10); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(세번째_페이지_인덱스_1_맥북_검색_id_9); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(세번째_페이지_인덱스_2_맥북_검색_id_8); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_네번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(3, 페이지_크기_3), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(네번째_페이지_인덱스_0_맥북_검색_id_7); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(네번째_페이지_인덱스_1_맥북_검색_id_4); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(네번째_페이지_인덱스_2_맥북_검색_id_3); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_다섯번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(4, 페이지_크기_3), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(2); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(다섯번째_페이지_인덱스_0_맥북_검색_id_2); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(다섯번째_페이지_인덱스_1_맥북_검색_id_1); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } + + @Test + void 페이지_크기_3_여섯번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(5, 페이지_크기_3), + 검색어_맥북 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isEmpty(); + softAssertions.assertThat(actual.getContent()).isEmpty(); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } + } + + @Nested + class 검색_결과가_존재하지_않는_검색어_테스트 { + + @Test + void 페이지_크기_3_첫번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(0, 페이지_크기_3), + 검색어_캐비어 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isEmpty(); + softAssertions.assertThat(actual.getContent()).isEmpty(); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSortByAuctioneerCountTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSortByAuctioneerCountTest.java new file mode 100644 index 000000000..5faf1e672 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSortByAuctioneerCountTest.java @@ -0,0 +1,151 @@ +package com.ddang.ddang.auction.infrastructure.persistence; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.infrastructure.persistence.fixture.AuctionForListSortByAuctioneerCountFixture; +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AuctionForListSortByAuctioneerCountTest extends AuctionForListSortByAuctioneerCountFixture { + + QuerydslAuctionRepository querydslAuctionRepository; + + @BeforeEach + void setUp(@Autowired final JPAQueryFactory queryFactory) { + querydslAuctionRepository = new QuerydslAuctionRepositoryImpl(queryFactory); + } + + @Test + void 페이지_크기_3_첫번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(0, 페이지_크기_3, 참여_인원순_정렬), + 검색어_없음 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(첫번째_페이지_인덱스_0_참여자_7_2일_후_마감_id_4); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(첫번째_페이지_인덱스_1_참여자_6_4일_후_마감_id_15); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(첫번째_페이지_인덱스_2_참여자_6_4일_후_마감_id_14); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_두번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(1, 페이지_크기_3, 참여_인원순_정렬), + 검색어_없음 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(두번째_페이지_인덱스_0_참여자_6_4일_후_마감_id_12); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(두번째_페이지_인덱스_1_참여자_6_4일_후_마감_id_11); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(두번째_페이지_인덱스_2_참여자_6_4일_후_마감_id_10); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_세번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(2, 페이지_크기_3, 참여_인원순_정렬), + 검색어_없음 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(세번째_페이지_인덱스_0_참여자_4_1일_후_마감_id_5); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(세번째_페이지_인덱스_1_참여자_4_3일_후_마감_id_3); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(세번째_페이지_인덱스_2_참여자_1_4일_후_마감_id_2); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_네번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(3, 페이지_크기_3, 참여_인원순_정렬), + 검색어_없음 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(네번째_페이지_인덱스_0_참여자_8_2일_전_마감_id_6); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(네번째_페이지_인덱스_1_참여자_6_4일_전_마감_id_16); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(네번째_페이지_인덱스_2_참여자_6_4일_전_마감_id_13); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_다섯번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(4, 페이지_크기_3, 참여_인원순_정렬), + 검색어_없음 + ); + + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(다섯번째_페이지_인덱스_0_참여자_6_4일_전_마감_id_9); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(다섯번째_페이지_인덱스_1_참여자_6_4일_전_마감_id_8); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(다섯번째_페이지_인덱스_2_참여자_3_4일_전_마감_id_7); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_여섯번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(5, 페이지_크기_3, 참여_인원순_정렬), + 검색어_없음 + ); + + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(1); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(여섯번째_페이지_인덱스_0_참여자_2_5일_전_마감_id_1); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } + + @Test + void 페이지_크기_3_일곱번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(6, 페이지_크기_3, 참여_인원순_정렬), + 검색어_없음 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isEmpty(); + softAssertions.assertThat(actual.getContent()).isEmpty(); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSortByClosingTimeTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSortByClosingTimeTest.java new file mode 100644 index 000000000..b94cac4b7 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSortByClosingTimeTest.java @@ -0,0 +1,151 @@ +package com.ddang.ddang.auction.infrastructure.persistence; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.infrastructure.persistence.fixture.AuctionForListSortByClosingTimeFixture; +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AuctionForListSortByClosingTimeTest extends AuctionForListSortByClosingTimeFixture { + + QuerydslAuctionRepository querydslAuctionRepository; + + @BeforeEach + void setUp(@Autowired final JPAQueryFactory queryFactory) { + querydslAuctionRepository = new QuerydslAuctionRepositoryImpl(queryFactory); + } + + @Test + void 페이지_크기_3_첫번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(0, 페이지_크기_3, 마감_임박순_정렬), + 검색어_없음 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(첫번째_페이지_인덱스_0_1일_후_마감_id_5); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(첫번째_페이지_인덱스_1_2일_후_마감_id_4); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(첫번째_페이지_인덱스_2_3일_후_마감_id_3); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_두번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(1, 페이지_크기_3, 마감_임박순_정렬), + 검색어_없음 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(두번째_페이지_인덱스_0_4일_후_마감_id_15); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(두번째_페이지_인덱스_1_4일_후_마감_id_14); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(두번째_페이지_인덱스_2_4일_후_마감_id_12); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_세번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(2, 페이지_크기_3, 마감_임박순_정렬), + 검색어_없음 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(세번째_페이지_인덱스_0_4일_후_마감_id_11); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(세번째_페이지_인덱스_1_4일_후_마감_id_10); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(세번째_페이지_인덱스_2_4일_후_마감_id_2); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_네번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(3, 페이지_크기_3, 마감_임박순_정렬), + 검색어_없음 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(네번째_페이지_인덱스_0_5일_전_마감_id_1); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(네번째_페이지_인덱스_1_4일_전_마감_id_16); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(네번째_페이지_인덱스_2_4일_전_마감_id_13); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_다섯번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(4, 페이지_크기_3, 마감_임박순_정렬), + 검색어_없음 + ); + + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(다섯번째_페이지_인덱스_0_4일_전_마감_id_9); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(다섯번째_페이지_인덱스_1_4일_전_마감_id_8); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(다섯번째_페이지_인덱스_2_4일_전_마감_id_7); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_여섯번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(5, 페이지_크기_3, 마감_임박순_정렬), + 검색어_없음 + ); + + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(1); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(여섯번째_페이지_인덱스_0_2일_전_마감_id_6); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } + + @Test + void 페이지_크기_3_일곱번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(6, 페이지_크기_3, 마감_임박순_정렬), + 검색어_없음 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isEmpty(); + softAssertions.assertThat(actual.getContent()).isEmpty(); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSortByIdTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSortByIdTest.java new file mode 100644 index 000000000..64eae857f --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSortByIdTest.java @@ -0,0 +1,151 @@ +package com.ddang.ddang.auction.infrastructure.persistence; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.infrastructure.persistence.fixture.AuctionForListSortByIdFixture; +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AuctionForListSortByIdTest extends AuctionForListSortByIdFixture { + + QuerydslAuctionRepository querydslAuctionRepository; + + @BeforeEach + void setUp(@Autowired final JPAQueryFactory queryFactory) { + querydslAuctionRepository = new QuerydslAuctionRepositoryImpl(queryFactory); + } + + @Test + void 페이지_크기_3_첫번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(0, 페이지_크기_3, id순_정렬), + 검색어_없음 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(첫번째_페이지_인덱스_0_id_16); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(첫번째_페이지_인덱스_1_id_15); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(첫번째_페이지_인덱스_2_id_14); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_두번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(1, 페이지_크기_3, id순_정렬), + 검색어_없음 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(두번째_페이지_인덱스_0_id_13); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(두번째_페이지_인덱스_1_id_12); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(두번째_페이지_인덱스_2_id_11); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_세번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(2, 페이지_크기_3, id순_정렬), + 검색어_없음 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(세번째_페이지_인덱스_0_id_10); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(세번째_페이지_인덱스_1_id_9); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(세번째_페이지_인덱스_2_id_8); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_네번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(3, 페이지_크기_3, id순_정렬), + 검색어_없음 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(네번째_페이지_인덱스_0_id_7); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(네번째_페이지_인덱스_1_id_6); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(네번째_페이지_인덱스_2_id_5); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_다섯번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(4, 페이지_크기_3, id순_정렬), + 검색어_없음 + ); + + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(다섯번째_페이지_인덱스_0_id_4); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(다섯번째_페이지_인덱스_1_id_3); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(다섯번째_페이지_인덱스_2_id_2); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_여섯번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(5, 페이지_크기_3, id순_정렬), + 검색어_없음 + ); + + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(1); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(여섯번째_페이지_인덱스_0_id_1); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } + + @Test + void 페이지_크기_3_일곱번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(6, 페이지_크기_3, id순_정렬), + 검색어_없음 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isEmpty(); + softAssertions.assertThat(actual.getContent()).isEmpty(); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSortByReliabilityTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSortByReliabilityTest.java new file mode 100644 index 000000000..c296e536d --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/AuctionForListSortByReliabilityTest.java @@ -0,0 +1,151 @@ +package com.ddang.ddang.auction.infrastructure.persistence; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.infrastructure.persistence.fixture.AuctionForListSortByReliabilityFixture; +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AuctionForListSortByReliabilityTest extends AuctionForListSortByReliabilityFixture { + + QuerydslAuctionRepository querydslAuctionRepository; + + @BeforeEach + void setUp(@Autowired final JPAQueryFactory queryFactory) { + querydslAuctionRepository = new QuerydslAuctionRepositoryImpl(queryFactory); + } + + @Test + void 페이지_크기_3_첫번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(0, 페이지_크기_3, 신뢰도순_정렬), + 검색어_없음 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(첫번째_페이지_인덱스_0_신뢰도_5_0_2일_후_마감_id_4); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(첫번째_페이지_인덱스_1_신뢰도_3_5_4일_후_마감_id_14); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(첫번째_페이지_인덱스_2_신뢰도_3_5_4일_후_마감_id_12); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_두번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(1, 페이지_크기_3, 신뢰도순_정렬), + 검색어_없음 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(두번째_페이지_인덱스_0_신뢰도_3_5_4일_후_마감_id_10); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(두번째_페이지_인덱스_1_신뢰도_2_1_4일_후_마감_id_15); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(두번째_페이지_인덱스_2_신뢰도_2_1_4일_후_마감_id_11); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_세번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(2, 페이지_크기_3, 신뢰도순_정렬), + 검색어_없음 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(세번째_페이지_인덱스_0_신뢰도_2_1_3일_후_마감_id_3); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(세번째_페이지_인덱스_1_신뢰도_2_1_4일_후_마감_id_2); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(세번째_페이지_인덱스_2_신뢰도_1_5_1일_후_마감_id_5); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_네번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(3, 페이지_크기_3, 신뢰도순_정렬), + 검색어_없음 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(네번째_페이지_인덱스_0_신뢰도_4_7_4일_전_마감_id_7); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(네번째_페이지_인덱스_1_신뢰도_4_7_5일_전_마감_id_1); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(네번째_페이지_인덱스_2_신뢰도_3_5_4일_전_마감_id_16); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_다섯번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(4, 페이지_크기_3, 신뢰도순_정렬), + 검색어_없음 + ); + + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(다섯번째_페이지_인덱스_0_신뢰도_3_5_4일_전_마감_id_8); + softAssertions.assertThat(actual.getContent().get(1)).isEqualTo(다섯번째_페이지_인덱스_1_신뢰도_2_1_4일_전_마감_id_13); + softAssertions.assertThat(actual.getContent().get(2)).isEqualTo(다섯번째_페이지_인덱스_2_신뢰도_2_1_4일_전_마감_id_9); + softAssertions.assertThat(actual.hasNext()).isTrue(); + }); + } + + @Test + void 페이지_크기_3_여섯번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(5, 페이지_크기_3, 신뢰도순_정렬), + 검색어_없음 + ); + + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(1); + softAssertions.assertThat(actual.getContent().get(0)).isEqualTo(여섯번째_페이지_인덱스_0_신뢰도_0_3_2일_전_마감_id_6); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } + + @Test + void 페이지_크기_3_일곱번째_페이지_요청_테스트() { + // when + final Slice actual = querydslAuctionRepository.findAuctionsAllByCondition( + PageRequest.of(6, 페이지_크기_3, 신뢰도순_정렬), + 검색어_없음 + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isEmpty(); + softAssertions.assertThat(actual.getContent()).isEmpty(); + softAssertions.assertThat(actual.hasNext()).isFalse(); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/JpaAuctionRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/JpaAuctionRepositoryTest.java index c92a7965f..dc415434e 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/JpaAuctionRepositoryTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/JpaAuctionRepositoryTest.java @@ -1,18 +1,9 @@ package com.ddang.ddang.auction.infrastructure.persistence; -import static org.assertj.core.api.Assertions.assertThat; - import com.ddang.ddang.auction.domain.Auction; -import com.ddang.ddang.auction.domain.BidUnit; -import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.fixture.JpaAuctionRepositoryFixture; import com.ddang.ddang.configuration.JpaConfiguration; import com.ddang.ddang.configuration.QuerydslConfiguration; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.Optional; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; @@ -21,69 +12,50 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + @DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -@Import({JpaConfiguration.class, QuerydslConfiguration.class}) -class JpaAuctionRepositoryTest { - - @PersistenceContext - EntityManager em; +class JpaAuctionRepositoryTest extends JpaAuctionRepositoryFixture { @Autowired JpaAuctionRepository auctionRepository; @Test void 경매를_저장한다() { - // given - final Auction auction = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - // when - auctionRepository.save(auction); + final Auction actual = auctionRepository.save(저장하기_전_경매_엔티티); - // then - em.flush(); - em.clear(); - - assertThat(auction.getId()).isPositive(); + assertThat(actual.getId()).isPositive(); } @Test void 지정한_아이디에_대한_경매를_조회한다() { - // given - final Instant instant = Instant.parse("2023-07-08T22:21:20Z"); - final ZoneId zoneId = ZoneId.of("UTC"); - final Auction expected = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(instant.atZone(zoneId).toLocalDateTime()) - .build(); - - auctionRepository.save(expected); - - em.flush(); - em.clear(); - // when - final Optional actual = auctionRepository.findById(expected.getId()); + final Optional actual = auctionRepository.findByIdAndDeletedIsFalse(저장된_경매_엔티티.getId()); // then SoftAssertions.assertSoftly(softAssertions -> { softAssertions.assertThat(actual).isPresent(); - softAssertions.assertThat(actual.get().getId()).isEqualTo(expected.getId()); - softAssertions.assertThat(actual.get().getTitle()).isEqualTo(expected.getTitle()); - softAssertions.assertThat(actual.get().getDescription()).isEqualTo(expected.getDescription()); - softAssertions.assertThat(actual.get().getBidUnit()).isEqualTo(expected.getBidUnit()); - softAssertions.assertThat(actual.get().getStartPrice()).isEqualTo(expected.getStartPrice()); - softAssertions.assertThat(actual.get().getClosingTime()).isEqualTo(expected.getClosingTime()); + softAssertions.assertThat(actual.get().getId()).isEqualTo(저장된_경매_엔티티.getId()); + softAssertions.assertThat(actual.get().getTitle()).isEqualTo(저장된_경매_엔티티.getTitle()); + softAssertions.assertThat(actual.get().getDescription()).isEqualTo(저장된_경매_엔티티.getDescription()); + softAssertions.assertThat(actual.get().getBidUnit()).isEqualTo(저장된_경매_엔티티.getBidUnit()); + softAssertions.assertThat(actual.get().getStartPrice()).isEqualTo(저장된_경매_엔티티.getStartPrice()); + softAssertions.assertThat(actual.get().getClosingTime()).isEqualTo(저장된_경매_엔티티.getClosingTime()); }); } + + @Test + void 삭제된_아이디에_대한_경매_조회시_빈_optional을_반환한다() { + // when + final Optional actual = auctionRepository.findByIdAndDeletedIsFalse(삭제된_경매_엔티티.getId()); + + // then + assertThat(actual).isEmpty(); + } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionAndImageRepositoryImplTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionAndImageRepositoryImplTest.java new file mode 100644 index 000000000..43d57c08e --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionAndImageRepositoryImplTest.java @@ -0,0 +1,37 @@ +package com.ddang.ddang.auction.infrastructure.persistence; + +import com.ddang.ddang.auction.infrastructure.persistence.dto.AuctionAndImageDto; +import com.ddang.ddang.auction.infrastructure.persistence.fixture.QuerydslAuctionAndImageRepositoryImplFixture; +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import java.util.Optional; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class QuerydslAuctionAndImageRepositoryImplTest extends QuerydslAuctionAndImageRepositoryImplFixture { + + @Autowired + JpaAuctionRepository auctionRepository; + + @Test + void 경매와_경매_대표이미지를_조회한다() { + // when + final Optional actual = auctionRepository.findDtoByAuctionId(경매.getId()); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isPresent(); + softAssertions.assertThat(actual.get().auction()).isEqualTo(경매); + softAssertions.assertThat(actual.get().auctionImage()).isEqualTo(경매_이미지); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionRepositoryImplForObjectTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionRepositoryImplForObjectTest.java new file mode 100644 index 000000000..f7d2ed39f --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionRepositoryImplForObjectTest.java @@ -0,0 +1,78 @@ +package com.ddang.ddang.auction.infrastructure.persistence; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.infrastructure.persistence.fixture.QuerydslAuctionRepositoryImplForObjectFixture; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.ddang.ddang.region.domain.Region; +import com.ddang.ddang.user.domain.User; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.Optional; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class QuerydslAuctionRepositoryImplForObjectTest extends QuerydslAuctionRepositoryImplForObjectFixture { + + QuerydslAuctionRepository querydslAuctionRepository; + + @BeforeEach + void setUp(@Autowired final JPAQueryFactory queryFactory) { + querydslAuctionRepository = new QuerydslAuctionRepositoryImpl(queryFactory); + } + + @Test + void 지정한_아이디에_대한_경매를_조회한다() { + // when + final Optional actual = querydslAuctionRepository.findAuctionById(경매.getId()); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isPresent(); + + final Auction actualAuction = actual.get(); + softAssertions.assertThat(actualAuction.getTitle()).isEqualTo(경매.getTitle()); + softAssertions.assertThat(actualAuction.getId()).isEqualTo(경매.getId()); + softAssertions.assertThat(actualAuction.getAuctionRegions()).hasSize(1); + + final Region actualThirdRegion = actualAuction.getAuctionRegions().get(0).getThirdRegion(); + softAssertions.assertThat(actualThirdRegion.getName()).isEqualTo(개포1동.getName()); + + final Region actualSecondRegion = actualThirdRegion.getSecondRegion(); + softAssertions.assertThat(actualSecondRegion.getName()).isEqualTo(강남구.getName()); + + final Region actualFirstRegion = actualSecondRegion.getFirstRegion(); + softAssertions.assertThat(actualFirstRegion.getName()).isEqualTo(서울특별시.getName()); + + final Category actualSubCategory = actual.get().getSubCategory(); + softAssertions.assertThat(actualSubCategory).isEqualTo(가구_서브_의자_카테고리); + + final Category mainCategory = actualSubCategory.getMainCategory(); + softAssertions.assertThat(mainCategory).isEqualTo(가구_카테고리); + + final User actualSeller = actual.get().getSeller(); + softAssertions.assertThat(actualSeller).isEqualTo(판매자); + }); + } + + @Test + void 지정한_아이디에_해당하는_경매가_없는_경우_빈_Optional을_조회한다() { + // when + final Optional actual = querydslAuctionRepository.findAuctionById(존재하지_않는_경매); + + // then + assertThat(actual).isEmpty(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionRepositoryImplTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionRepositoryImplTest.java deleted file mode 100644 index 684c95ff1..000000000 --- a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionRepositoryImplTest.java +++ /dev/null @@ -1,285 +0,0 @@ -package com.ddang.ddang.auction.infrastructure.persistence; - -import com.ddang.ddang.auction.domain.Auction; -import com.ddang.ddang.auction.domain.BidUnit; -import com.ddang.ddang.auction.domain.Price; -import com.ddang.ddang.category.domain.Category; -import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; -import com.ddang.ddang.configuration.JpaConfiguration; -import com.ddang.ddang.configuration.QuerydslConfiguration; -import com.ddang.ddang.region.domain.AuctionRegion; -import com.ddang.ddang.region.domain.Region; -import com.ddang.ddang.region.infrastructure.persistence.JpaRegionRepository; -import com.ddang.ddang.user.domain.User; -import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; -import com.querydsl.jpa.impl.JPAQueryFactory; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; -import org.springframework.data.domain.Slice; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; - -@DataJpaTest -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -@SuppressWarnings("NonAsciiCharacters") -@Import({JpaConfiguration.class, QuerydslConfiguration.class}) -class QuerydslAuctionRepositoryImplTest { - - @PersistenceContext - EntityManager em; - - @Autowired - JpaUserRepository userRepository; - - @Autowired - JpaAuctionRepository auctionRepository; - - @Autowired - JpaRegionRepository regionRepository; - - @Autowired - JpaCategoryRepository categoryRepository; - - QuerydslAuctionRepository querydslAuctionRepository; - - @BeforeEach - void setUp(@Autowired final JPAQueryFactory queryFactory) { - querydslAuctionRepository = new QuerydslAuctionRepositoryImpl(queryFactory); - } - - @Test - void 지정한_아이디에_대한_경매를_조회한다() { - // given - final User seller = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - userRepository.save(seller); - - final Category main = new Category("main"); - final Category sub = new Category("sub"); - - main.addSubCategory(sub); - - categoryRepository.save(main); - - final Auction auction = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .subCategory(sub) - .seller(seller) - .build(); - - final Region firstRegion = new Region("서울특별시"); - final Region secondRegion = new Region("강남구"); - final Region thirdRegion = new Region("개포1동"); - - secondRegion.addThirdRegion(thirdRegion); - firstRegion.addSecondRegion(secondRegion); - - regionRepository.save(firstRegion); - final AuctionRegion auctionRegion = new AuctionRegion(thirdRegion); - - auction.addAuctionRegions(List.of(auctionRegion)); - - auctionRepository.save(auction); - - em.flush(); - em.clear(); - - // when - final Optional actual = querydslAuctionRepository.findAuctionById(auction.getId()); - - // then - SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual).isPresent(); - - final Auction actualAuction = actual.get(); - softAssertions.assertThat(actualAuction.getTitle()).isEqualTo(auction.getTitle()); - softAssertions.assertThat(actualAuction.getId()).isEqualTo(auction.getId()); - softAssertions.assertThat(actualAuction.getAuctionRegions()).hasSize(1); - - final Region actualThirdRegion = actualAuction.getAuctionRegions().get(0).getThirdRegion(); - softAssertions.assertThat(actualThirdRegion.getName()).isEqualTo(thirdRegion.getName()); - - final Region actualSecondRegion = actualThirdRegion.getSecondRegion(); - softAssertions.assertThat(actualSecondRegion.getName()).isEqualTo(secondRegion.getName()); - - final Region actualFirstRegion = actualSecondRegion.getFirstRegion(); - softAssertions.assertThat(actualFirstRegion.getName()).isEqualTo(firstRegion.getName()); - - final Category subCategory = actual.get().getSubCategory(); - softAssertions.assertThat(subCategory).isEqualTo(sub); - - final Category mainCategory = subCategory.getMainCategory(); - softAssertions.assertThat(mainCategory).isEqualTo(main); - - final User actualSeller = actual.get().getSeller(); - softAssertions.assertThat(actualSeller).isEqualTo(seller); - }); - } - - @Test - void 지정한_아이디에_해당하는_경매가_없는_경우_빈_Optional을_조회한다() { - // given - final Long invalidId = -999L; - - // when - final Optional actual = querydslAuctionRepository.findAuctionById(invalidId); - - // then - assertThat(actual).isEmpty(); - } - - @Test - void 첫번째_페이지의_경매_목록을_조회한다() { - // given - final Auction auction1 = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final Auction auction2 = Auction.builder() - .title("경매 상품 2") - .description("이것은 경매 상품 2 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final Auction auction3 = Auction.builder() - .title("경매 상품 3") - .description("이것은 경매 상품 3 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - - auctionRepository.save(auction1); - auctionRepository.save(auction2); - auctionRepository.save(auction3); - - em.flush(); - em.clear(); - - // when - final Slice actual = auctionRepository.findAuctionsAllByLastAuctionId(null, 1); - - // then - SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual).hasSize(1); - - final List actualAuctions = actual.getContent(); - softAssertions.assertThat(actualAuctions.get(0).getTitle()).isEqualTo(auction3.getTitle()); - }); - } - - @Test - void 두번째_페이지의_경매_목록을_조회한다() { - // given - final Auction auction1 = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final Auction auction2 = Auction.builder() - .title("경매 상품 2") - .description("이것은 경매 상품 2 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final Auction auction3 = Auction.builder() - .title("경매 상품 3") - .description("이것은 경매 상품 3 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - - auctionRepository.save(auction1); - auctionRepository.save(auction2); - auctionRepository.save(auction3); - - em.flush(); - em.clear(); - - // when - - final Slice actual = auctionRepository.findAuctionsAllByLastAuctionId(auction3.getId(), 1); - - // then - SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual).hasSize(1); - - final List actualAuctions = actual.getContent(); - softAssertions.assertThat(actualAuctions.get(0).getTitle()).isEqualTo(auction2.getTitle()); - }); - } - - @Test - void 두번째_페이지의_삭제된_경매를_제외한_목록을_조회한다() { - // given - final Auction auction1 = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final Auction auction2 = Auction.builder() - .title("경매 상품 2") - .description("이것은 경매 상품 2 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final Auction auction3 = Auction.builder() - .title("경매 상품 3") - .description("이것은 경매 상품 3 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - - auctionRepository.save(auction1); - auctionRepository.save(auction2); - auctionRepository.save(auction3); - - auction2.delete(); - - em.flush(); - em.clear(); - - // when - final Slice actual = auctionRepository.findAuctionsAllByLastAuctionId(auction3.getId(), 1); - - // then - SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual).hasSize(1); - - final List actualAuctions = actual.getContent(); - softAssertions.assertThat(actualAuctions.get(0).getTitle()).isEqualTo(auction1.getTitle()); - }); - } -} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListByBidderIdFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListByBidderIdFixture.java new file mode 100644 index 000000000..4d0b1f940 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListByBidderIdFixture.java @@ -0,0 +1,34 @@ +package com.ddang.ddang.auction.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.user.domain.User; +import org.junit.jupiter.api.BeforeEach; + +@SuppressWarnings("NonAsciiCharacters") +public class AuctionForListByBidderIdFixture extends QuerydslAuctionRepositoryImplForListFixture { + + protected User 참여한_경매가_7개인_사용자; + protected Auction 첫번째_페이지_인덱스_0; + protected Auction 첫번째_페이지_인덱스_1; + protected Auction 첫번째_페이지_인덱스_2; + protected Auction 두번째_페이지_인덱스_0; + protected Auction 두번째_페이지_인덱스_1; + protected Auction 두번째_페이지_인덱스_2; + protected Auction 세번째_페이지_인덱스_0; + + protected User 참여한_경매가_없는_사용자 = 판매자_0_3점_2; + + @BeforeEach + void fixtureSetUp() { + 참여한_경매가_7개인_사용자 = 판매자_4_7점; + 첫번째_페이지_인덱스_0 = 경매16; + 첫번째_페이지_인덱스_1 = 경매1; + 첫번째_페이지_인덱스_2 = 경매7; + 두번째_페이지_인덱스_0 = 경매13; + 두번째_페이지_인덱스_1 = 경매9; + 두번째_페이지_인덱스_2 = 경매8; + 세번째_페이지_인덱스_0 = 경매6; + + 참여한_경매가_없는_사용자 = 판매자_0_3점_2; + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListByUserIdFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListByUserIdFixture.java new file mode 100644 index 000000000..57cef12fa --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListByUserIdFixture.java @@ -0,0 +1,30 @@ +package com.ddang.ddang.auction.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.user.domain.User; +import org.junit.jupiter.api.BeforeEach; + +@SuppressWarnings("NonAsciiCharacters") +public class AuctionForListByUserIdFixture extends QuerydslAuctionRepositoryImplForListFixture { + + protected User 등록한_경매가_5개인_사용자; + protected Auction 첫번째_페이지_인덱스_0; + protected Auction 첫번째_페이지_인덱스_1; + protected Auction 첫번째_페이지_인덱스_2; + protected Auction 두번째_페이지_인덱스_0; + protected Auction 두번째_페이지_인덱스_1; + + protected User 등록한_경매가_없는_사용자; + + @BeforeEach + void fixtureSetUp() { + 등록한_경매가_5개인_사용자 = 판매자_3_5점; + 첫번째_페이지_인덱스_0 = 경매16; + 첫번째_페이지_인덱스_1 = 경매14; + 첫번째_페이지_인덱스_2 = 경매12; + 두번째_페이지_인덱스_0 = 경매10; + 두번째_페이지_인덱스_1 = 경매8; + + 등록한_경매가_없는_사용자 = 판매자_0_3점_2; + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSearchByTitleAndSortByAuctioneerCountFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSearchByTitleAndSortByAuctioneerCountFixture.java new file mode 100644 index 000000000..8d5a0f148 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSearchByTitleAndSortByAuctioneerCountFixture.java @@ -0,0 +1,47 @@ +package com.ddang.ddang.auction.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.presentation.dto.request.ReadAuctionSearchCondition; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; + +@SuppressWarnings("NonAsciiCharacters") +public class AuctionForListSearchByTitleAndSortByAuctioneerCountFixture extends QuerydslAuctionRepositoryImplForListFixture { + + protected Sort 참여_인원순_정렬 = Sort.by(Order.asc("auctioneerCount")); + protected ReadAuctionSearchCondition 검색어_맥북 = new ReadAuctionSearchCondition("맥북"); + + protected Auction 첫번째_페이지_인덱스_0_참여자_7_2일_후_마감_id_4; + protected Auction 첫번째_페이지_인덱스_1_참여자_6_4일_후_마감_id_15; + protected Auction 첫번째_페이지_인덱스_2_참여자_6_4일_후_마감_id_14; + protected Auction 두번째_페이지_인덱스_0_참여자_6_4일_후_마감_id_12; + protected Auction 두번째_페이지_인덱스_1_참여자_6_4일_후_마감_id_11; + protected Auction 두번째_페이지_인덱스_2_참여자_6_4일_후_마감_id_10; + protected Auction 세번째_페이지_인덱스_0_참여자_4_3일_후_마감_id_3; + protected Auction 세번째_페이지_인덱스_1_참여자_1_4일_후_마감_id_2; + protected Auction 세번째_페이지_인덱스_2_참여자_6_4일_전_마감_id_16; + protected Auction 네번째_페이지_인덱스_0_참여자_6_4일_전_마감_id_13; + protected Auction 네번째_페이지_인덱스_1_참여자_6_4일_전_마감_id_9; + protected Auction 네번째_페이지_인덱스_2_참여자_6_4일_전_마감_id_8; + protected Auction 다섯번째_페이지_인덱스_0_참여자_3_4일_전_마감_id_7; + protected Auction 다섯번째_페이지_인덱스_1_참여자_2_5일_전_마감_id_1; + + @BeforeEach + void fixtureSetUp() { + 첫번째_페이지_인덱스_0_참여자_7_2일_후_마감_id_4 = 경매4; + 첫번째_페이지_인덱스_1_참여자_6_4일_후_마감_id_15 = 경매15; + 첫번째_페이지_인덱스_2_참여자_6_4일_후_마감_id_14 = 경매14; + 두번째_페이지_인덱스_0_참여자_6_4일_후_마감_id_12 = 경매12; + 두번째_페이지_인덱스_1_참여자_6_4일_후_마감_id_11 = 경매11; + 두번째_페이지_인덱스_2_참여자_6_4일_후_마감_id_10 = 경매10; + 세번째_페이지_인덱스_0_참여자_4_3일_후_마감_id_3 = 경매3; + 세번째_페이지_인덱스_1_참여자_1_4일_후_마감_id_2 = 경매2; + 세번째_페이지_인덱스_2_참여자_6_4일_전_마감_id_16 = 경매16; + 네번째_페이지_인덱스_0_참여자_6_4일_전_마감_id_13 = 경매13; + 네번째_페이지_인덱스_1_참여자_6_4일_전_마감_id_9 = 경매9; + 네번째_페이지_인덱스_2_참여자_6_4일_전_마감_id_8 = 경매8; + 다섯번째_페이지_인덱스_0_참여자_3_4일_전_마감_id_7 = 경매7; + 다섯번째_페이지_인덱스_1_참여자_2_5일_전_마감_id_1 = 경매1; + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSearchByTitleAndSortByClosingTimeFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSearchByTitleAndSortByClosingTimeFixture.java new file mode 100644 index 000000000..b11603ab9 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSearchByTitleAndSortByClosingTimeFixture.java @@ -0,0 +1,47 @@ +package com.ddang.ddang.auction.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.presentation.dto.request.ReadAuctionSearchCondition; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; + +@SuppressWarnings("NonAsciiCharacters") +public class AuctionForListSearchByTitleAndSortByClosingTimeFixture extends QuerydslAuctionRepositoryImplForListFixture { + + protected Sort 마감_임박순_정렬 = Sort.by(Order.asc("closingTime")); + protected ReadAuctionSearchCondition 검색어_맥북 = new ReadAuctionSearchCondition("맥북"); + + protected Auction 첫번째_페이지_인덱스_0_2일_후_마감_id_4; + protected Auction 첫번째_페이지_인덱스_1_3일_후_마감_id_3; + protected Auction 첫번째_페이지_인덱스_2_4일_후_마감_id_15; + protected Auction 두번째_페이지_인덱스_0_4일_후_마감_id_14; + protected Auction 두번째_페이지_인덱스_1_4일_후_마감_id_12; + protected Auction 두번째_페이지_인덱스_2_4일_후_마감_id_11; + protected Auction 세번째_페이지_인덱스_0_4일_후_마감_id_10; + protected Auction 세번째_페이지_인덱스_1_4일_후_마감_id_2; + protected Auction 세번째_페이지_인덱스_2_5일_전_마감_id_1; + protected Auction 네번째_페이지_인덱스_0_4일_전_마감_id_16; + protected Auction 네번째_페이지_인덱스_1_4일_전_마감_id_13; + protected Auction 네번째_페이지_인덱스_2_4일_전_마감_id_9; + protected Auction 다섯번째_페이지_인덱스_0_4일_전_마감_id_8; + protected Auction 다섯번째_페이지_인덱스_1_4일_전_마감_id_7; + + @BeforeEach + void fixtureSetUp() { + 첫번째_페이지_인덱스_0_2일_후_마감_id_4 = 경매4; + 첫번째_페이지_인덱스_1_3일_후_마감_id_3 = 경매3; + 첫번째_페이지_인덱스_2_4일_후_마감_id_15 = 경매15; + 두번째_페이지_인덱스_0_4일_후_마감_id_14 = 경매14; + 두번째_페이지_인덱스_1_4일_후_마감_id_12 = 경매12; + 두번째_페이지_인덱스_2_4일_후_마감_id_11 = 경매11; + 세번째_페이지_인덱스_0_4일_후_마감_id_10 = 경매10; + 세번째_페이지_인덱스_1_4일_후_마감_id_2 = 경매2; + 세번째_페이지_인덱스_2_5일_전_마감_id_1 = 경매1; + 네번째_페이지_인덱스_0_4일_전_마감_id_16 = 경매16; + 네번째_페이지_인덱스_1_4일_전_마감_id_13 = 경매13; + 네번째_페이지_인덱스_2_4일_전_마감_id_9 = 경매9; + 다섯번째_페이지_인덱스_0_4일_전_마감_id_8 = 경매8; + 다섯번째_페이지_인덱스_1_4일_전_마감_id_7 = 경매7; + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSearchByTitleAndSortByIdFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSearchByTitleAndSortByIdFixture.java new file mode 100644 index 000000000..d580201a9 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSearchByTitleAndSortByIdFixture.java @@ -0,0 +1,47 @@ +package com.ddang.ddang.auction.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.presentation.dto.request.ReadAuctionSearchCondition; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; + +@SuppressWarnings("NonAsciiCharacters") +public class AuctionForListSearchByTitleAndSortByIdFixture extends QuerydslAuctionRepositoryImplForListFixture { + + protected Sort id순_정렬 = Sort.by(Order.asc("id")); + protected ReadAuctionSearchCondition 검색어_맥북 = new ReadAuctionSearchCondition("맥북"); + + protected Auction 첫번째_페이지_인덱스_0_id_16; + protected Auction 첫번째_페이지_인덱스_1_id_15; + protected Auction 첫번째_페이지_인덱스_2_id_14; + protected Auction 두번째_페이지_인덱스_0_id_13; + protected Auction 두번째_페이지_인덱스_1_id_12; + protected Auction 두번째_페이지_인덱스_2_id_11; + protected Auction 세번째_페이지_인덱스_0_id_10; + protected Auction 세번째_페이지_인덱스_1_id_9; + protected Auction 세번째_페이지_인덱스_2_id_8; + protected Auction 네번째_페이지_인덱스_0_id_7; + protected Auction 네번째_페이지_인덱스_1_id_4; + protected Auction 네번째_페이지_인덱스_2_id_3; + protected Auction 다섯번째_페이지_인덱스_0_id_2; + protected Auction 다섯번째_페이지_인덱스_1_id_1; + + @BeforeEach + void fixtureSetUp() { + 첫번째_페이지_인덱스_0_id_16 = 경매16; + 첫번째_페이지_인덱스_1_id_15 = 경매15; + 첫번째_페이지_인덱스_2_id_14 = 경매14; + 두번째_페이지_인덱스_0_id_13 = 경매13; + 두번째_페이지_인덱스_1_id_12 = 경매12; + 두번째_페이지_인덱스_2_id_11 = 경매11; + 세번째_페이지_인덱스_0_id_10 = 경매10; + 세번째_페이지_인덱스_1_id_9 = 경매9; + 세번째_페이지_인덱스_2_id_8 = 경매8; + 네번째_페이지_인덱스_0_id_7 = 경매7; + 네번째_페이지_인덱스_1_id_4 = 경매4; + 네번째_페이지_인덱스_2_id_3 = 경매3; + 다섯번째_페이지_인덱스_0_id_2 = 경매2; + 다섯번째_페이지_인덱스_1_id_1 = 경매1; + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSearchByTitleAndSortByReliabilityFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSearchByTitleAndSortByReliabilityFixture.java new file mode 100644 index 000000000..dfa71bef5 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSearchByTitleAndSortByReliabilityFixture.java @@ -0,0 +1,47 @@ +package com.ddang.ddang.auction.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.presentation.dto.request.ReadAuctionSearchCondition; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; + +@SuppressWarnings("NonAsciiCharacters") +public class AuctionForListSearchByTitleAndSortByReliabilityFixture extends QuerydslAuctionRepositoryImplForListFixture { + + protected Sort 신뢰도순_정렬 = Sort.by(Order.asc("reliability")); + protected ReadAuctionSearchCondition 검색어_맥북 = new ReadAuctionSearchCondition("맥북"); + + protected Auction 첫번째_페이지_인덱스_0_신뢰도_5_0_2일_후_마감_id_4; + protected Auction 첫번째_페이지_인덱스_1_신뢰도_3_5_4일_후_마감_id_14; + protected Auction 첫번째_페이지_인덱스_2_신뢰도_3_5_4일_후_마감_id_12; + protected Auction 두번째_페이지_인덱스_0_신뢰도_3_5_4일_후_마감_id_10; + protected Auction 두번째_페이지_인덱스_1_신뢰도_2_1_4일_후_마감_id_15; + protected Auction 두번째_페이지_인덱스_2_신뢰도_2_1_4일_후_마감_id_11; + protected Auction 세번째_페이지_인덱스_0_신뢰도_2_1_3일_후_마감_id_3; + protected Auction 세번째_페이지_인덱스_1_신뢰도_2_1_4일_후_마감_id_2; + protected Auction 세번째_페이지_인덱스_2_신뢰도_4_7_4일_전_마감_id_7; + protected Auction 네번째_페이지_인덱스_0_신뢰도_4_7_5일_전_마감_id_1; + protected Auction 네번째_페이지_인덱스_1_신뢰도_3_5_4일_전_마감_id_16; + protected Auction 네번째_페이지_인덱스_2_신뢰도_3_5_4일_전_마감_id_8; + protected Auction 다섯번째_페이지_인덱스_0_신뢰도_2_1_4일_전_마감_id_13; + protected Auction 다섯번째_페이지_인덱스_1_신뢰도_2_1_4일_전_마감_id_9; + + @BeforeEach + void fixtureSetUp() { + 첫번째_페이지_인덱스_0_신뢰도_5_0_2일_후_마감_id_4 = 경매4; + 첫번째_페이지_인덱스_1_신뢰도_3_5_4일_후_마감_id_14 = 경매14; + 첫번째_페이지_인덱스_2_신뢰도_3_5_4일_후_마감_id_12 = 경매12; + 두번째_페이지_인덱스_0_신뢰도_3_5_4일_후_마감_id_10 = 경매10; + 두번째_페이지_인덱스_1_신뢰도_2_1_4일_후_마감_id_15 = 경매15; + 두번째_페이지_인덱스_2_신뢰도_2_1_4일_후_마감_id_11 = 경매11; + 세번째_페이지_인덱스_0_신뢰도_2_1_3일_후_마감_id_3 = 경매3; + 세번째_페이지_인덱스_1_신뢰도_2_1_4일_후_마감_id_2 = 경매2; + 세번째_페이지_인덱스_2_신뢰도_4_7_4일_전_마감_id_7 = 경매7; + 네번째_페이지_인덱스_0_신뢰도_4_7_5일_전_마감_id_1 = 경매1; + 네번째_페이지_인덱스_1_신뢰도_3_5_4일_전_마감_id_16 = 경매16; + 네번째_페이지_인덱스_2_신뢰도_3_5_4일_전_마감_id_8 = 경매8; + 다섯번째_페이지_인덱스_0_신뢰도_2_1_4일_전_마감_id_13 = 경매13; + 다섯번째_페이지_인덱스_1_신뢰도_2_1_4일_전_마감_id_9 = 경매9; + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSearchByTitleFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSearchByTitleFixture.java new file mode 100644 index 000000000..2c7928f3c --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSearchByTitleFixture.java @@ -0,0 +1,46 @@ +package com.ddang.ddang.auction.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.presentation.dto.request.ReadAuctionSearchCondition; +import org.junit.jupiter.api.BeforeEach; + +@SuppressWarnings("NonAsciiCharacters") +public class AuctionForListSearchByTitleFixture extends QuerydslAuctionRepositoryImplForListFixture { + + protected ReadAuctionSearchCondition 검색어_맥북 = new ReadAuctionSearchCondition("맥북"); + + protected Auction 첫번째_페이지_인덱스_0_맥북_검색_id_16; + protected Auction 첫번째_페이지_인덱스_1_맥북_검색_id_15; + protected Auction 첫번째_페이지_인덱스_2_맥북_검색_id_14; + protected Auction 두번째_페이지_인덱스_0_맥북_검색_id_13; + protected Auction 두번째_페이지_인덱스_1_맥북_검색_id_12; + protected Auction 두번째_페이지_인덱스_2_맥북_검색_id_11; + protected Auction 세번째_페이지_인덱스_0_맥북_검색_id_10; + protected Auction 세번째_페이지_인덱스_1_맥북_검색_id_9; + protected Auction 세번째_페이지_인덱스_2_맥북_검색_id_8; + protected Auction 네번째_페이지_인덱스_0_맥북_검색_id_7; + protected Auction 네번째_페이지_인덱스_1_맥북_검색_id_4; + protected Auction 네번째_페이지_인덱스_2_맥북_검색_id_3; + protected Auction 다섯번째_페이지_인덱스_0_맥북_검색_id_2; + protected Auction 다섯번째_페이지_인덱스_1_맥북_검색_id_1; + + protected ReadAuctionSearchCondition 검색어_캐비어 = new ReadAuctionSearchCondition("캐비어"); + + @BeforeEach + void fixtureSetUp() { + 첫번째_페이지_인덱스_0_맥북_검색_id_16 = 경매16; + 첫번째_페이지_인덱스_1_맥북_검색_id_15 = 경매15; + 첫번째_페이지_인덱스_2_맥북_검색_id_14 = 경매14; + 두번째_페이지_인덱스_0_맥북_검색_id_13 = 경매13; + 두번째_페이지_인덱스_1_맥북_검색_id_12 = 경매12; + 두번째_페이지_인덱스_2_맥북_검색_id_11 = 경매11; + 세번째_페이지_인덱스_0_맥북_검색_id_10 = 경매10; + 세번째_페이지_인덱스_1_맥북_검색_id_9 = 경매9; + 세번째_페이지_인덱스_2_맥북_검색_id_8 = 경매8; + 네번째_페이지_인덱스_0_맥북_검색_id_7 = 경매7; + 네번째_페이지_인덱스_1_맥북_검색_id_4 = 경매4; + 네번째_페이지_인덱스_2_맥북_검색_id_3 = 경매3; + 다섯번째_페이지_인덱스_0_맥북_검색_id_2 = 경매2; + 다섯번째_페이지_인덱스_1_맥북_검색_id_1 = 경매1; + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSortByAuctioneerCountFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSortByAuctioneerCountFixture.java new file mode 100644 index 000000000..7d836df44 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSortByAuctioneerCountFixture.java @@ -0,0 +1,51 @@ +package com.ddang.ddang.auction.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.presentation.dto.request.ReadAuctionSearchCondition; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; + +@SuppressWarnings("NonAsciiCharacters") +public class AuctionForListSortByAuctioneerCountFixture extends QuerydslAuctionRepositoryImplForListFixture { + + protected Sort 참여_인원순_정렬 = Sort.by(Order.asc("auctioneerCount")); + protected ReadAuctionSearchCondition 검색어_없음 = new ReadAuctionSearchCondition(null); + + protected Auction 첫번째_페이지_인덱스_0_참여자_7_2일_후_마감_id_4; + protected Auction 첫번째_페이지_인덱스_1_참여자_6_4일_후_마감_id_15; + protected Auction 첫번째_페이지_인덱스_2_참여자_6_4일_후_마감_id_14; + protected Auction 두번째_페이지_인덱스_0_참여자_6_4일_후_마감_id_12; + protected Auction 두번째_페이지_인덱스_1_참여자_6_4일_후_마감_id_11; + protected Auction 두번째_페이지_인덱스_2_참여자_6_4일_후_마감_id_10; + protected Auction 세번째_페이지_인덱스_0_참여자_4_1일_후_마감_id_5; + protected Auction 세번째_페이지_인덱스_1_참여자_4_3일_후_마감_id_3; + protected Auction 세번째_페이지_인덱스_2_참여자_1_4일_후_마감_id_2; + protected Auction 네번째_페이지_인덱스_0_참여자_8_2일_전_마감_id_6; + protected Auction 네번째_페이지_인덱스_1_참여자_6_4일_전_마감_id_16; + protected Auction 네번째_페이지_인덱스_2_참여자_6_4일_전_마감_id_13; + protected Auction 다섯번째_페이지_인덱스_0_참여자_6_4일_전_마감_id_9; + protected Auction 다섯번째_페이지_인덱스_1_참여자_6_4일_전_마감_id_8; + protected Auction 다섯번째_페이지_인덱스_2_참여자_3_4일_전_마감_id_7; + protected Auction 여섯번째_페이지_인덱스_0_참여자_2_5일_전_마감_id_1; + + @BeforeEach + void fixtureSetUp() { + 첫번째_페이지_인덱스_0_참여자_7_2일_후_마감_id_4 = 경매4; + 첫번째_페이지_인덱스_1_참여자_6_4일_후_마감_id_15 = 경매15; + 첫번째_페이지_인덱스_2_참여자_6_4일_후_마감_id_14 = 경매14; + 두번째_페이지_인덱스_0_참여자_6_4일_후_마감_id_12 = 경매12; + 두번째_페이지_인덱스_1_참여자_6_4일_후_마감_id_11 = 경매11; + 두번째_페이지_인덱스_2_참여자_6_4일_후_마감_id_10 = 경매10; + 세번째_페이지_인덱스_0_참여자_4_1일_후_마감_id_5 = 경매5; + 세번째_페이지_인덱스_1_참여자_4_3일_후_마감_id_3 = 경매3; + 세번째_페이지_인덱스_2_참여자_1_4일_후_마감_id_2 = 경매2; + 네번째_페이지_인덱스_0_참여자_8_2일_전_마감_id_6 = 경매6; + 네번째_페이지_인덱스_1_참여자_6_4일_전_마감_id_16 = 경매16; + 네번째_페이지_인덱스_2_참여자_6_4일_전_마감_id_13 = 경매13; + 다섯번째_페이지_인덱스_0_참여자_6_4일_전_마감_id_9 = 경매9; + 다섯번째_페이지_인덱스_1_참여자_6_4일_전_마감_id_8 = 경매8; + 다섯번째_페이지_인덱스_2_참여자_3_4일_전_마감_id_7 = 경매7; + 여섯번째_페이지_인덱스_0_참여자_2_5일_전_마감_id_1 = 경매1; + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSortByClosingTimeFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSortByClosingTimeFixture.java new file mode 100644 index 000000000..e7cb94697 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSortByClosingTimeFixture.java @@ -0,0 +1,51 @@ +package com.ddang.ddang.auction.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.presentation.dto.request.ReadAuctionSearchCondition; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; + +@SuppressWarnings("NonAsciiCharacters") +public class AuctionForListSortByClosingTimeFixture extends QuerydslAuctionRepositoryImplForListFixture { + + protected Sort 마감_임박순_정렬 = Sort.by(Order.asc("closingTime")); + protected ReadAuctionSearchCondition 검색어_없음 = new ReadAuctionSearchCondition(null); + + protected Auction 첫번째_페이지_인덱스_0_1일_후_마감_id_5; + protected Auction 첫번째_페이지_인덱스_1_2일_후_마감_id_4; + protected Auction 첫번째_페이지_인덱스_2_3일_후_마감_id_3; + protected Auction 두번째_페이지_인덱스_0_4일_후_마감_id_15; + protected Auction 두번째_페이지_인덱스_1_4일_후_마감_id_14; + protected Auction 두번째_페이지_인덱스_2_4일_후_마감_id_12; + protected Auction 세번째_페이지_인덱스_0_4일_후_마감_id_11; + protected Auction 세번째_페이지_인덱스_1_4일_후_마감_id_10; + protected Auction 세번째_페이지_인덱스_2_4일_후_마감_id_2; + protected Auction 네번째_페이지_인덱스_0_5일_전_마감_id_1; + protected Auction 네번째_페이지_인덱스_1_4일_전_마감_id_16; + protected Auction 네번째_페이지_인덱스_2_4일_전_마감_id_13; + protected Auction 다섯번째_페이지_인덱스_0_4일_전_마감_id_9; + protected Auction 다섯번째_페이지_인덱스_1_4일_전_마감_id_8; + protected Auction 다섯번째_페이지_인덱스_2_4일_전_마감_id_7; + protected Auction 여섯번째_페이지_인덱스_0_2일_전_마감_id_6; + + @BeforeEach + void fixtureSetUp() { + 첫번째_페이지_인덱스_0_1일_후_마감_id_5 = 경매5; + 첫번째_페이지_인덱스_1_2일_후_마감_id_4 = 경매4; + 첫번째_페이지_인덱스_2_3일_후_마감_id_3 = 경매3; + 두번째_페이지_인덱스_0_4일_후_마감_id_15 = 경매15; + 두번째_페이지_인덱스_1_4일_후_마감_id_14 = 경매14; + 두번째_페이지_인덱스_2_4일_후_마감_id_12 = 경매12; + 세번째_페이지_인덱스_0_4일_후_마감_id_11 = 경매11; + 세번째_페이지_인덱스_1_4일_후_마감_id_10 = 경매10; + 세번째_페이지_인덱스_2_4일_후_마감_id_2 = 경매2; + 네번째_페이지_인덱스_0_5일_전_마감_id_1 = 경매1; + 네번째_페이지_인덱스_1_4일_전_마감_id_16 = 경매16; + 네번째_페이지_인덱스_2_4일_전_마감_id_13 = 경매13; + 다섯번째_페이지_인덱스_0_4일_전_마감_id_9 = 경매9; + 다섯번째_페이지_인덱스_1_4일_전_마감_id_8 = 경매8; + 다섯번째_페이지_인덱스_2_4일_전_마감_id_7 = 경매7; + 여섯번째_페이지_인덱스_0_2일_전_마감_id_6 = 경매6; + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSortByIdFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSortByIdFixture.java new file mode 100644 index 000000000..bfae846c0 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSortByIdFixture.java @@ -0,0 +1,51 @@ +package com.ddang.ddang.auction.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.presentation.dto.request.ReadAuctionSearchCondition; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; + +@SuppressWarnings("NonAsciiCharacters") +public class AuctionForListSortByIdFixture extends QuerydslAuctionRepositoryImplForListFixture { + + protected Sort id순_정렬 = Sort.by(Order.asc("id")); + protected ReadAuctionSearchCondition 검색어_없음 = new ReadAuctionSearchCondition(null); + + protected Auction 첫번째_페이지_인덱스_0_id_16; + protected Auction 첫번째_페이지_인덱스_1_id_15; + protected Auction 첫번째_페이지_인덱스_2_id_14; + protected Auction 두번째_페이지_인덱스_0_id_13; + protected Auction 두번째_페이지_인덱스_1_id_12; + protected Auction 두번째_페이지_인덱스_2_id_11; + protected Auction 세번째_페이지_인덱스_0_id_10; + protected Auction 세번째_페이지_인덱스_1_id_9; + protected Auction 세번째_페이지_인덱스_2_id_8; + protected Auction 네번째_페이지_인덱스_0_id_7; + protected Auction 네번째_페이지_인덱스_1_id_6; + protected Auction 네번째_페이지_인덱스_2_id_5; + protected Auction 다섯번째_페이지_인덱스_0_id_4; + protected Auction 다섯번째_페이지_인덱스_1_id_3; + protected Auction 다섯번째_페이지_인덱스_2_id_2; + protected Auction 여섯번째_페이지_인덱스_0_id_1; + + @BeforeEach + void fixtureSetUp() { + 첫번째_페이지_인덱스_0_id_16 = 경매16; + 첫번째_페이지_인덱스_1_id_15 = 경매15; + 첫번째_페이지_인덱스_2_id_14 = 경매14; + 두번째_페이지_인덱스_0_id_13 = 경매13; + 두번째_페이지_인덱스_1_id_12 = 경매12; + 두번째_페이지_인덱스_2_id_11 = 경매11; + 세번째_페이지_인덱스_0_id_10 = 경매10; + 세번째_페이지_인덱스_1_id_9 = 경매9; + 세번째_페이지_인덱스_2_id_8 = 경매8; + 네번째_페이지_인덱스_0_id_7 = 경매7; + 네번째_페이지_인덱스_1_id_6 = 경매6; + 네번째_페이지_인덱스_2_id_5 = 경매5; + 다섯번째_페이지_인덱스_0_id_4 = 경매4; + 다섯번째_페이지_인덱스_1_id_3 = 경매3; + 다섯번째_페이지_인덱스_2_id_2 = 경매2; + 여섯번째_페이지_인덱스_0_id_1 = 경매1; + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSortByReliabilityFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSortByReliabilityFixture.java new file mode 100644 index 000000000..45f0a9b7a --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/AuctionForListSortByReliabilityFixture.java @@ -0,0 +1,51 @@ +package com.ddang.ddang.auction.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.presentation.dto.request.ReadAuctionSearchCondition; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; + +@SuppressWarnings("NonAsciiCharacters") +public class AuctionForListSortByReliabilityFixture extends QuerydslAuctionRepositoryImplForListFixture { + + protected Sort 신뢰도순_정렬 = Sort.by(Order.asc("reliability")); + protected ReadAuctionSearchCondition 검색어_없음 = new ReadAuctionSearchCondition(null); + + protected Auction 첫번째_페이지_인덱스_0_신뢰도_5_0_2일_후_마감_id_4; + protected Auction 첫번째_페이지_인덱스_1_신뢰도_3_5_4일_후_마감_id_14; + protected Auction 첫번째_페이지_인덱스_2_신뢰도_3_5_4일_후_마감_id_12; + protected Auction 두번째_페이지_인덱스_0_신뢰도_3_5_4일_후_마감_id_10; + protected Auction 두번째_페이지_인덱스_1_신뢰도_2_1_4일_후_마감_id_15; + protected Auction 두번째_페이지_인덱스_2_신뢰도_2_1_4일_후_마감_id_11; + protected Auction 세번째_페이지_인덱스_0_신뢰도_2_1_3일_후_마감_id_3; + protected Auction 세번째_페이지_인덱스_1_신뢰도_2_1_4일_후_마감_id_2; + protected Auction 세번째_페이지_인덱스_2_신뢰도_1_5_1일_후_마감_id_5; + protected Auction 네번째_페이지_인덱스_0_신뢰도_4_7_4일_전_마감_id_7; + protected Auction 네번째_페이지_인덱스_1_신뢰도_4_7_5일_전_마감_id_1; + protected Auction 네번째_페이지_인덱스_2_신뢰도_3_5_4일_전_마감_id_16; + protected Auction 다섯번째_페이지_인덱스_0_신뢰도_3_5_4일_전_마감_id_8; + protected Auction 다섯번째_페이지_인덱스_1_신뢰도_2_1_4일_전_마감_id_13; + protected Auction 다섯번째_페이지_인덱스_2_신뢰도_2_1_4일_전_마감_id_9; + protected Auction 여섯번째_페이지_인덱스_0_신뢰도_0_3_2일_전_마감_id_6; + + @BeforeEach + void fixtureSetUp() { + 첫번째_페이지_인덱스_0_신뢰도_5_0_2일_후_마감_id_4 = 경매4; + 첫번째_페이지_인덱스_1_신뢰도_3_5_4일_후_마감_id_14 = 경매14; + 첫번째_페이지_인덱스_2_신뢰도_3_5_4일_후_마감_id_12 = 경매12; + 두번째_페이지_인덱스_0_신뢰도_3_5_4일_후_마감_id_10 = 경매10; + 두번째_페이지_인덱스_1_신뢰도_2_1_4일_후_마감_id_15 = 경매15; + 두번째_페이지_인덱스_2_신뢰도_2_1_4일_후_마감_id_11 = 경매11; + 세번째_페이지_인덱스_0_신뢰도_2_1_3일_후_마감_id_3 = 경매3; + 세번째_페이지_인덱스_1_신뢰도_2_1_4일_후_마감_id_2 = 경매2; + 세번째_페이지_인덱스_2_신뢰도_1_5_1일_후_마감_id_5 = 경매5; + 네번째_페이지_인덱스_0_신뢰도_4_7_4일_전_마감_id_7 = 경매7; + 네번째_페이지_인덱스_1_신뢰도_4_7_5일_전_마감_id_1 = 경매1; + 네번째_페이지_인덱스_2_신뢰도_3_5_4일_전_마감_id_16 = 경매16; + 다섯번째_페이지_인덱스_0_신뢰도_3_5_4일_전_마감_id_8 = 경매8; + 다섯번째_페이지_인덱스_1_신뢰도_2_1_4일_전_마감_id_13 = 경매13; + 다섯번째_페이지_인덱스_2_신뢰도_2_1_4일_전_마감_id_9 = 경매9; + 여섯번째_페이지_인덱스_0_신뢰도_0_3_2일_전_마감_id_6 = 경매6; + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/JpaAuctionRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/JpaAuctionRepositoryFixture.java new file mode 100644 index 000000000..7e09374d8 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/JpaAuctionRepositoryFixture.java @@ -0,0 +1,60 @@ +package com.ddang.ddang.auction.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +@SuppressWarnings("NonAsciiCharacters") +public class JpaAuctionRepositoryFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaAuctionRepository auctionRepository; + + private Instant 시간 = Instant.parse("2023-07-08T22:21:20Z"); + private ZoneId 위치 = ZoneId.of("UTC"); + + protected Auction 저장하기_전_경매_엔티티 = Auction.builder() + .title("제목") + .description("내용") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + protected Auction 저장된_경매_엔티티 = Auction.builder() + .title("경매 상품 1") + .description("이것은 경매 상품 1 입니다.") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(시간.atZone(위치).toLocalDateTime()) + .build(); + protected Auction 삭제된_경매_엔티티 = Auction.builder() + .title("경매 상품 1") + .description("이것은 경매 상품 1 입니다.") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(시간.atZone(위치).toLocalDateTime()) + .build(); + + @BeforeEach + void setUp() { + 삭제된_경매_엔티티.delete(); + + auctionRepository.saveAll(List.of(저장된_경매_엔티티, 삭제된_경매_엔티티)); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/QuerydslAuctionAndImageRepositoryImplFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/QuerydslAuctionAndImageRepositoryImplFixture.java new file mode 100644 index 000000000..485cbfeaa --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/QuerydslAuctionAndImageRepositoryImplFixture.java @@ -0,0 +1,66 @@ +package com.ddang.ddang.auction.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.image.infrastructure.persistence.JpaAuctionImageRepository; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class QuerydslAuctionAndImageRepositoryImplFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaAuctionImageRepository auctionImageRepository; + + @Autowired + private JpaUserRepository userRepository; + + private User 사용자; + protected Auction 경매; + protected AuctionImage 경매_이미지; + + @BeforeEach + void setUp() { + 경매 = Auction.builder() + .title("경매 상품 1") + .description("이것은 경매 상품 1 입니다.") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 경매_이미지 = new AuctionImage("upload.png", "store.png"); + auctionImageRepository.save(경매_이미지); + 경매.addAuctionImages(List.of(경매_이미지)); + + 사용자 = User.builder() + .name("사용자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + + auctionRepository.save(경매); + userRepository.save(사용자); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/QuerydslAuctionRepositoryImplForListFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/QuerydslAuctionRepositoryImplForListFixture.java new file mode 100644 index 000000000..b293a72ed --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/QuerydslAuctionRepositoryImplForListFixture.java @@ -0,0 +1,317 @@ +package com.ddang.ddang.auction.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.bid.domain.Bid; +import com.ddang.ddang.bid.domain.BidPrice; +import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class QuerydslAuctionRepositoryImplForListFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaCategoryRepository categoryRepository; + + @Autowired + private JpaBidRepository bidRepository; + + protected User 판매자_4_7점; + protected User 판매자_3_5점; + protected User 판매자_2_1점; + protected User 판매자_5_0점; + protected User 판매자_1_5점; + protected User 판매자_0_3점_1; + protected User 판매자_0_3점_2; + + protected Auction 경매1; + protected Auction 경매2; + protected Auction 경매3; + protected Auction 경매4; + protected Auction 경매5; + protected Auction 경매6; + protected Auction 경매7; + protected Auction 경매8; + protected Auction 경매9; + protected Auction 경매10; + protected Auction 경매11; + protected Auction 경매12; + protected Auction 경매13; + protected Auction 경매14; + protected Auction 경매15; + protected Auction 경매16; + + protected int 페이지_크기_3 = 3; + + @BeforeEach + void commonFixtureSetUp() { + 판매자_4_7점 = User.builder() + .name("판매자 4.7점") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("1") + .build(); + 판매자_3_5점 = User.builder() + .name("판매자 3.5점") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(3.5d)) + .oauthId("2") + .build(); + 판매자_2_1점 = User.builder() + .name("판매자 2.1점 ") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(2.1d)) + .oauthId("3") + .build(); + 판매자_5_0점 = User.builder() + .name("판매자 5.0점") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(5.0d)) + .oauthId("4") + .build(); + 판매자_1_5점 = User.builder() + .name("판매자 1.5점") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(1.5d)) + .oauthId("5") + .build(); + 판매자_0_3점_1 = User.builder() + .name("판매자 0.3점 1") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(0.3d)) + .oauthId("6") + .build(); + 판매자_0_3점_2 = User.builder() + .name("판매자 0.3점 2") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(0.3d)) + .oauthId("7") + .build(); + + userRepository.saveAll(List.of(판매자_4_7점, 판매자_3_5점, 판매자_2_1점, 판매자_5_0점, 판매자_1_5점, 판매자_0_3점_1, 판매자_0_3점_2)); + + final Category 기타_카테고리 = new Category("기타"); + final Category 기타_서브_기타_카테고리 = new Category("기타"); + + 기타_카테고리.addSubCategory(기타_서브_기타_카테고리); + + categoryRepository.save(기타_카테고리); + + final LocalDateTime 현재시간 = LocalDateTime.now(); + + 경매6 = Auction.builder() + .title("레드불을 팝니다") + .description("레드불을 팝니다") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(현재시간.minusDays(2)) + .seller(판매자_0_3점_1) + .build(); + bidding(경매6, 판매자_4_7점); + addAuctioneerCount(경매6, 7); + 경매8 = Auction.builder() + .title("맥북을 팝니다") + .description("맥북을 팝니다") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(현재시간.minusDays(4)) + .seller(판매자_3_5점) // ㅇㅋ + .build(); + bidding(경매8, 판매자_4_7점); + addAuctioneerCount(경매8, 5); + 경매16 = Auction.builder() + .title("맥북을 팝니다") + .description("맥북을 팝니다") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(현재시간.minusDays(4)) + .seller(판매자_3_5점) // ㅇㅋ + .build(); + bidding(경매16, 판매자_4_7점); + addAuctioneerCount(경매16, 4); + 경매9 = Auction.builder() + .title("맥북을 팝니다") + .description("맥북을 팝니다") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(현재시간.minusDays(4)) + .seller(판매자_2_1점) + .build(); + bidding(경매9, 판매자_4_7점); + addAuctioneerCount(경매9, 5); + 경매13 = Auction.builder() + .title("맥북을 팝니다") + .description("맥북을 팝니다") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(현재시간.minusDays(4)) + .seller(판매자_2_1점) + .build(); + bidding(경매13, 판매자_4_7점); + addAuctioneerCount(경매13, 5); + 경매7 = Auction.builder() + .title("맥북을 팝니다") + .description("맥북1을 팝니다") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(현재시간.minusDays(3)) + .seller(판매자_4_7점) + .build(); + addAuctioneerCount(경매7, 3); + 경매1 = Auction.builder() + .title("맥북에어를 팝니다") + .description("맥북에어를 팝니다") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(현재시간.minusDays(5)) + .seller(판매자_4_7점) + .build(); + addAuctioneerCount(경매1, 2); + bidding(경매16, 판매자_4_7점); + 경매2 = Auction.builder() + .title("맥북프로를 팝니다") + .description("맥북프로를 팝니다") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(현재시간.plusDays(4)) + .seller(판매자_2_1점) + .build(); + addAuctioneerCount(경매2, 1); + 경매3 = Auction.builder() + .title("맥북뭐시기를 팝니다") + .description("맥북뭐시기를 팝니다") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(현재시간.plusDays(3)) + .seller(판매자_2_1점) + .build(); + addAuctioneerCount(경매3, 4); + 경매4 = Auction.builder() + .title("집에가고싶은 맥북을 팝니다") + .description("집에가고싶은 맥북을 팝니다") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(현재시간.plusDays(2)) + .seller(판매자_5_0점) + .build(); + addAuctioneerCount(경매4, 7); + 경매5 = Auction.builder() + .title("핫식스를 팝니다") + .description("핫식스를 팝니다") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(현재시간.plusDays(1)) + .seller(판매자_1_5점) + .build(); + addAuctioneerCount(경매5, 4); + 경매10 = Auction.builder() + .title("맥북을 팝니다") + .description("맥북을 팝니다") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(현재시간.plusDays(4)) + .seller(판매자_3_5점) // ㅇㅋ + .build(); + addAuctioneerCount(경매10, 6); + 경매11 = Auction.builder() + .title("맥북을 팝니다") + .description("맥북을 팝니다") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(현재시간.plusDays(4)) + .seller(판매자_2_1점) + .build(); + addAuctioneerCount(경매11, 6); + 경매12 = Auction.builder() + .title("맥북을 팝니다") + .description("맥북을 팝니다") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(현재시간.plusDays(4)) + .seller(판매자_3_5점) // ㅇㅋ + .build(); + addAuctioneerCount(경매12, 6); + 경매14 = Auction.builder() + .title("맥북을 팝니다") + .description("맥북을 팝니다") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(현재시간.plusDays(4)) + .seller(판매자_3_5점) // ㅇㅋ + .build(); + addAuctioneerCount(경매14, 6); + 경매15 = Auction.builder() + .title("맥북을 팝니다") + .description("맥북을 팝니다") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(현재시간.plusDays(4)) + .seller(판매자_2_1점) + .build(); + addAuctioneerCount(경매15, 6); + + final List 경매들 = List.of( + 경매1, + 경매2, + 경매3, + 경매4, + 경매5, + 경매6, + 경매7, + 경매8, + 경매9, + 경매10, + 경매11, + 경매12, + 경매13, + 경매14, + 경매15, + 경매16 + ); + auctionRepository.saveAll(경매들); + + em.flush(); + em.clear(); + } + + private void bidding(final Auction targetAuction, final User bidder) { + final Bid lastBid = new Bid(targetAuction, bidder, new BidPrice(1)); + + bidRepository.save(lastBid); + + targetAuction.updateLastBid(lastBid); + } + + private void addAuctioneerCount(final Auction targetAuction, final int count) { + final Bid lastBid = new Bid(targetAuction, targetAuction.getSeller(), new BidPrice(1)); + + bidRepository.save(lastBid); + + for (int i = 0; i < count; i++) { + targetAuction.updateLastBid(lastBid); + } + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/QuerydslAuctionRepositoryImplForObjectFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/QuerydslAuctionRepositoryImplForObjectFixture.java new file mode 100644 index 000000000..617113574 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/infrastructure/persistence/fixture/QuerydslAuctionRepositoryImplForObjectFixture.java @@ -0,0 +1,92 @@ +package com.ddang.ddang.auction.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.region.domain.AuctionRegion; +import com.ddang.ddang.region.domain.Region; +import com.ddang.ddang.region.infrastructure.persistence.JpaRegionRepository; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class QuerydslAuctionRepositoryImplForObjectFixture { + + @PersistenceContext + EntityManager em; + + @Autowired + JpaUserRepository userRepository; + + @Autowired + JpaAuctionRepository auctionRepository; + + @Autowired + JpaRegionRepository regionRepository; + + @Autowired + JpaCategoryRepository categoryRepository; + + protected Long 존재하지_않는_경매 = -999L; + protected Auction 경매; + protected Region 서울특별시; + protected Region 강남구; + protected Region 개포1동; + protected Category 가구_카테고리; + protected Category 가구_서브_의자_카테고리; + protected User 판매자; + + @BeforeEach + void totalFixtureSetUp() { + 판매자 = User.builder() + .name("판매자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + userRepository.save(판매자); + + 가구_카테고리 = new Category("가구"); + 가구_서브_의자_카테고리 = new Category("의자"); + + 가구_카테고리.addSubCategory(가구_서브_의자_카테고리); + + categoryRepository.save(가구_카테고리); + + 경매 = Auction.builder() + .title("제목") + .description("설명") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .subCategory(가구_서브_의자_카테고리) + .seller(판매자) + .build(); + + 서울특별시 = new Region("서울특별시"); + 강남구 = new Region("강남구"); + 개포1동 = new Region("개포1동"); + + 강남구.addThirdRegion(개포1동); + 서울특별시.addSecondRegion(강남구); + + regionRepository.save(서울특별시); + final AuctionRegion 직거래_지역 = new AuctionRegion(개포1동); + + 경매.addAuctionRegions(List.of(직거래_지역)); + + auctionRepository.save(경매); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionControllerTest.java index ddff1d478..cc415afe8 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionControllerTest.java @@ -1,8 +1,8 @@ package com.ddang.ddang.auction.presentation; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -27,97 +27,45 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.ddang.ddang.auction.application.AuctionService; import com.ddang.ddang.auction.application.dto.CreateAuctionDto; -import com.ddang.ddang.auction.application.dto.CreateInfoAuctionDto; -import com.ddang.ddang.auction.application.dto.ReadAuctionDto; -import com.ddang.ddang.auction.application.dto.ReadAuctionWithChatRoomIdDto; -import com.ddang.ddang.auction.application.dto.ReadAuctionsDto; -import com.ddang.ddang.auction.application.dto.ReadChatRoomDto; -import com.ddang.ddang.auction.application.dto.ReadRegionDto; -import com.ddang.ddang.auction.application.dto.ReadRegionsDto; import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; -import com.ddang.ddang.auction.presentation.dto.request.CreateAuctionRequest; -import com.ddang.ddang.authentication.application.AuthenticationUserService; -import com.ddang.ddang.authentication.application.BlackListTokenService; +import com.ddang.ddang.auction.configuration.DescendingSortPageableArgumentResolver; +import com.ddang.ddang.auction.presentation.dto.request.ReadAuctionSearchCondition; +import com.ddang.ddang.auction.presentation.fixture.AuctionControllerFixture; import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; import com.ddang.ddang.authentication.domain.TokenDecoder; import com.ddang.ddang.authentication.domain.TokenType; import com.ddang.ddang.authentication.domain.dto.AuthenticationStore; -import com.ddang.ddang.authentication.domain.dto.AuthenticationUserInfo; -import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims; import com.ddang.ddang.category.application.exception.CategoryNotFoundException; -import com.ddang.ddang.configuration.RestDocsConfiguration; import com.ddang.ddang.exception.GlobalExceptionHandler; import com.ddang.ddang.image.infrastructure.local.exception.EmptyImageException; import com.ddang.ddang.image.infrastructure.local.exception.StoreImageFailureException; import com.ddang.ddang.image.infrastructure.local.exception.UnsupportedImageFileExtensionException; import com.ddang.ddang.region.application.exception.RegionNotFoundException; import com.ddang.ddang.user.application.exception.UserNotFoundException; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.time.LocalDateTime; -import java.util.List; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; -import org.springframework.context.annotation.Import; +import org.springframework.data.domain.Pageable; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@WebMvcTest(controllers = {AuctionController.class}, - excludeFilters = { - @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfigurer.class), - @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com\\.ddang\\.ddang\\.authentication\\.configuration\\..*") - } -) -@AutoConfigureRestDocs -@Import(RestDocsConfiguration.class) -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -@SuppressWarnings("NonAsciiCharacters") -class AuctionControllerTest { - - @MockBean - AuctionService auctionService; - - @MockBean - BlackListTokenService blackListTokenService; - - @MockBean - AuthenticationUserService authenticationUserService; - - @Autowired - AuctionController auctionController; - - @Autowired - RestDocumentationResultHandler restDocs; - @Autowired - ObjectMapper objectMapper; +@SuppressWarnings("NonAsciiCharacters") +class AuctionControllerTest extends AuctionControllerFixture { TokenDecoder mockTokenDecoder; MockMvc mockMvc; @BeforeEach - void setUp(@Autowired RestDocumentationContextProvider provider) { + void setUp() { mockTokenDecoder = mock(TokenDecoder.class); final AuthenticationStore store = new AuthenticationStore(); @@ -132,7 +80,7 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { mockMvc = MockMvcBuilders.standaloneSetup(auctionController) .setControllerAdvice(new GlobalExceptionHandler()) .addInterceptors(interceptor) - .setCustomArgumentResolvers(resolver) + .setCustomArgumentResolvers(resolver, new DescendingSortPageableArgumentResolver()) .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) .alwaysDo(print()) .alwaysDo(restDocs) @@ -142,106 +90,42 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { @Test void 경매를_등록한다() throws Exception { // given - final MockMultipartFile auctionImage = new MockMultipartFile( - "images", - "image.png", - MediaType.IMAGE_PNG_VALUE, - new byte[]{1} - ); - final CreateAuctionRequest createAuctionRequest = new CreateAuctionRequest( - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - LocalDateTime.now().plusDays(3L), - 2L, - List.of(3L) - ); - final MockMultipartFile request = new MockMultipartFile( - "request", - "request", - MediaType.APPLICATION_JSON_VALUE, - objectMapper.writeValueAsBytes(createAuctionRequest) - ); - final CreateInfoAuctionDto createInfoAuctionDto = new CreateInfoAuctionDto( - 1L, - "title", - 1L, - 1_000 - ); - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(auctionService.create(any(CreateAuctionDto.class))).willReturn(createInfoAuctionDto); - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); + given(auctionService.create(any(CreateAuctionDto.class))).willReturn(경매_등록_결과_dto); + given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(유효한_사용자_id_클레임)); // when & then - mockMvc.perform(multipart("/auctions") - .file(auctionImage) - .file(request) - .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - ) - .andExpectAll( - status().isCreated(), - header().string(HttpHeaders.LOCATION, is("/auctions/1")), - jsonPath("$.id", is(1L), Long.class) - ) - .andDo( - restDocs.document( - requestHeaders( - headerWithName("Authorization").description("회원 Bearer 인증 정보") - ), - requestParts( - partWithName("images").description("이미지 파일(Array, 최대 10장)"), - partWithName("request").description("요청 데이터") - ), - responseFields( - fieldWithPath("id").type(JsonFieldType.NUMBER).description("경매 ID"), - fieldWithPath("title").type(JsonFieldType.STRING).description("경매 글 제목"), - fieldWithPath("image").type(JsonFieldType.STRING).description("경매 대표 이미지"), - fieldWithPath("auctionPrice").type(JsonFieldType.NUMBER).description("시작가"), - fieldWithPath("status").type(JsonFieldType.STRING).description("경매 상태"), - fieldWithPath("auctioneerCount").type(JsonFieldType.NUMBER).description("경매 참여자 수") - ) - ) - ); + final ResultActions resultAuctions = mockMvc.perform(multipart("/auctions") + .file(유효한_경매_이미지_파일) + .file(유효한_경매_등록_request_multipartFile) + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) + .header(HttpHeaders.AUTHORIZATION, 유효한_액세스_토큰) + ) + .andExpectAll( + status().isCreated(), + header().string(HttpHeaders.LOCATION, is("/auctions/1")), + jsonPath("$.id", is(경매_등록_결과_dto.id()), Long.class), + jsonPath("$.title", is(경매_등록_결과_dto.title())), + jsonPath("$.image", containsString(경매_이미지_상대_주소)), + jsonPath("$.auctionPrice", is(경매_등록_결과_dto.startPrice())), + jsonPath("$.status", is(경매_생성_시_경매_상태)), + jsonPath("$.auctioneerCount", is(경매_생성_시_경매_참여자_수)) + ); + + create_문서화(resultAuctions); } @Test void 경매_등록시_유효한_회원이_아니라면_404을_반환한다() throws Exception { // given - final MockMultipartFile auctionImage = new MockMultipartFile( - "images", - "image.png", - MediaType.IMAGE_PNG_VALUE, - new byte[]{1} - ); - final CreateAuctionRequest createAuctionRequest = new CreateAuctionRequest( - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - LocalDateTime.now().plusDays(3L), - 2L, - List.of(3L) - ); - final MockMultipartFile request = new MockMultipartFile( - "request", - "request", - MediaType.APPLICATION_JSON_VALUE, - objectMapper.writeValueAsBytes(createAuctionRequest) - ); - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); + given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(유효하지_않은_사용자_id_클레임)); given(auctionService.create(any())).willThrow(new UserNotFoundException("지정한 판매자를 찾을 수 없습니다.")); // when & then mockMvc.perform(multipart("/auctions") - .file(auctionImage) - .file(request) + .file(유효한_경매_이미지_파일) + .file(유효한_경매_등록_request_multipartFile) .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 유효한_액세스_토큰) ) .andExpectAll( status().isNotFound(), @@ -252,87 +136,41 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { @Test void 경매_등록시_유효한_하위_카테고리가_아니라면_400을_반환한다() throws Exception { // given - final MockMultipartFile auctionImage = new MockMultipartFile( - "images", - "image.png", - MediaType.IMAGE_PNG_VALUE, - new byte[]{1} - ); - final CreateAuctionRequest createAuctionRequest = new CreateAuctionRequest( - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - LocalDateTime.now().plusDays(3L), - 2L, - List.of(3L) - ); - final MockMultipartFile request = new MockMultipartFile( - "request", - "request", - MediaType.APPLICATION_JSON_VALUE, - objectMapper.writeValueAsBytes(createAuctionRequest) - ); - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); + given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(유효한_사용자_id_클레임)); given(auctionService.create(any())).willThrow( - new CategoryNotFoundException("지정한 판매자를 찾을 수 없습니다.") + new CategoryNotFoundException("지정한 카테고리를 찾을 수 없습니다.") ); // when & then mockMvc.perform(multipart("/auctions") - .file(auctionImage) - .file(request) + .file(유효한_경매_이미지_파일) + .file(유효하지_않은_카테고리_경매_등록_request_multipartFile) .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 유효한_액세스_토큰) ) .andExpectAll( - status().isNotFound(), + status().isBadRequest(), jsonPath("$.message").exists() ); } @Test - void 경매_등록시_유효한_세번째_지역이_아니라면_404을_반환한다() throws Exception { + void 경매_등록시_유효한_세번째_지역이_아니라면_400을_반환한다() throws Exception { // given - final MockMultipartFile auctionImage = new MockMultipartFile( - "images", - "image.png", - MediaType.IMAGE_PNG_VALUE, - new byte[]{1} - ); - final CreateAuctionRequest createAuctionRequest = new CreateAuctionRequest( - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - LocalDateTime.now().plusDays(3L), - 2L, - List.of(3L) - ); - final MockMultipartFile request = new MockMultipartFile( - "request", - "request", - MediaType.APPLICATION_JSON_VALUE, - objectMapper.writeValueAsBytes(createAuctionRequest) - ); - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); + given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(유효한_사용자_id_클레임)); given(auctionService.create(any())).willThrow( new RegionNotFoundException("지정한 세 번째 지역이 없거나 세 번째 지역이 아닙니다.") ); // when & then mockMvc.perform(multipart("/auctions") - .file(auctionImage) - .file(request) + .file(유효한_경매_이미지_파일) + .file(유효하지_않은_지역_경매_등록_request_multipartFile) .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 유효한_액세스_토큰) ) .andExpectAll( - status().isNotFound(), + status().isBadRequest(), jsonPath("$.message").exists() ); } @@ -340,40 +178,17 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { @Test void 경매_등록시_이미지_파일의_데이터가_비어있다면_400을_반환한다() throws Exception { // given - final MockMultipartFile auctionImage = new MockMultipartFile( - "images", - "image.png", - MediaType.IMAGE_PNG_VALUE, - new byte[]{1} - ); - final CreateAuctionRequest createAuctionRequest = new CreateAuctionRequest( - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - LocalDateTime.now().plusDays(3L), - 2L, - List.of(3L) - ); - final MockMultipartFile request = new MockMultipartFile( - "request", - "request", - MediaType.APPLICATION_JSON_VALUE, - objectMapper.writeValueAsBytes(createAuctionRequest) - ); - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); + given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(유효한_사용자_id_클레임)); given(auctionService.create(any())).willThrow( new EmptyImageException("이미지 파일의 데이터가 비어 있습니다.") ); // when & then mockMvc.perform(multipart("/auctions") - .file(auctionImage) - .file(request) + .file(비어_있는_경매_이미지_파일) + .file(비어있는_경매_이미지_경매_등록_request_multipartFile) .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 유효한_액세스_토큰) ) .andExpectAll( status().isBadRequest(), @@ -384,40 +199,17 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { @Test void 경매_등록시_이미지_저장에_실패하면_500을_반환한다() throws Exception { // given - final MockMultipartFile auctionImage = new MockMultipartFile( - "images", - "image.png", - MediaType.IMAGE_PNG_VALUE, - new byte[]{1} - ); - final CreateAuctionRequest createAuctionRequest = new CreateAuctionRequest( - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - LocalDateTime.now().plusDays(3L), - 2L, - List.of(3L) - ); - final MockMultipartFile request = new MockMultipartFile( - "request", - "request", - MediaType.APPLICATION_JSON_VALUE, - objectMapper.writeValueAsBytes(createAuctionRequest) - ); - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); + given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(유효한_사용자_id_클레임)); given(auctionService.create(any())).willThrow( new StoreImageFailureException("이미지 저장에 실패했습니다.", null) ); // when & then mockMvc.perform(multipart("/auctions") - .file(auctionImage) - .file(request) + .file(유효한_경매_이미지_파일) + .file(유효한_경매_등록_request_multipartFile) .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 유효한_액세스_토큰) ) .andExpectAll( status().isInternalServerError(), @@ -428,40 +220,17 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { @Test void 경매_등록시_지원하지_않는_확장자의_이미지면_400을_반환한다() throws Exception { // given - final MockMultipartFile auctionImage = new MockMultipartFile( - "images", - "image.png", - MediaType.IMAGE_PNG_VALUE, - new byte[]{1} - ); - final CreateAuctionRequest createAuctionRequest = new CreateAuctionRequest( - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - LocalDateTime.now().plusDays(3L), - 2L, - List.of(3L) - ); - final MockMultipartFile request = new MockMultipartFile( - "request", - "request", - MediaType.APPLICATION_JSON_VALUE, - objectMapper.writeValueAsBytes(createAuctionRequest) - ); - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); + given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(유효한_사용자_id_클레임)); given(auctionService.create(any())).willThrow( new UnsupportedImageFileExtensionException("지원하지 않는 확장자입니다. : ") ); // when & then mockMvc.perform(multipart("/auctions") - .file(auctionImage) - .file(request) + .file(유효하지_않은_확장자_경매_이미지_파일) + .file(유효한_경매_등록_request_multipartFile) .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 유효한_액세스_토큰) ) .andExpectAll( status().isBadRequest(), @@ -472,110 +241,57 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { @Test void 지정한_아이디에_해당하는_경매를_조회한다() throws Exception { // given - final ReadRegionsDto readRegionsDto = new ReadRegionsDto( - new ReadRegionDto(1L, "서울특별시"), - new ReadRegionDto(2L, "강서구"), - new ReadRegionDto(3L, "역삼동") - ); - final ReadAuctionDto auction = new ReadAuctionDto( - 1L, - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - null, - false, - LocalDateTime.now(), - LocalDateTime.now(), - List.of(readRegionsDto), - List.of(1L), - 2, - "main", - "sub", - 1L, - "https://profile.com", - "판매자", - 3.5d - ); - final ReadChatRoomDto chatRoomDto = new ReadChatRoomDto(1L, true); - - final ReadAuctionWithChatRoomIdDto auctionWithChatRoomIdDto = - new ReadAuctionWithChatRoomIdDto(auction, chatRoomDto); - - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(auctionService.readByAuctionId(anyLong(), any(AuthenticationUserInfo.class))) - .willReturn(auctionWithChatRoomIdDto); + given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(유효한_사용자_id_클레임)); + given(auctionService.readByAuctionId(anyLong())).willReturn(경매_조회_dto); + given(chatRoomService.readChatInfoByAuctionId(anyLong(), any())).willReturn(쪽지방_dto); // when & then - mockMvc.perform(RestDocumentationRequestBuilders.get("/auctions/{auctionId}", auction.id()) - .contentType(MediaType.APPLICATION_JSON) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - ) - .andExpectAll( - status().isOk(), - jsonPath("$.auction.id", is(auction.id()), Long.class), - jsonPath("$.auction.title", is(auction.title())), - jsonPath("$.auction.description", is(auction.description())), - jsonPath("$.auction.bidUnit", is(auction.bidUnit())), - jsonPath("$.auction.startPrice", is(auction.startPrice())), - jsonPath("$.auction.registerTime").exists(), - jsonPath("$.auction.closingTime").exists(), - jsonPath("$.auction.auctioneerCount", is(auction.auctioneerCount())) - ) - .andDo( - restDocs.document( - pathParameters( - parameterWithName("auctionId").description("조회하고자 하는 경매 ID") - ), - responseFields( - fieldWithPath("auction.id").type(JsonFieldType.NUMBER).description("경매 글 ID"), - fieldWithPath("auction.images").type(JsonFieldType.ARRAY).description("경매 이미지"), - fieldWithPath("auction.title").type(JsonFieldType.STRING).description("경매 글 제목"), - fieldWithPath("auction.category").type(JsonFieldType.OBJECT).description("경매 카테고리"), - fieldWithPath("auction.category.main").type(JsonFieldType.STRING).description("상위 카테고리"), - fieldWithPath("auction.category.sub").type(JsonFieldType.STRING).description("하위 카테고리"), - fieldWithPath("auction.description").type(JsonFieldType.STRING).description("경매 본문"), - fieldWithPath("auction.startPrice").type(JsonFieldType.NUMBER).description("시작가"), - fieldWithPath("auction.lastBidPrice").description("마지막 입찰가"), - fieldWithPath("auction.status").description("경매 상태"), - fieldWithPath("auction.bidUnit").type(JsonFieldType.NUMBER).description("입찰 단위"), - fieldWithPath("auction.registerTime").type(JsonFieldType.STRING).description("경매 등록시간"), - fieldWithPath("auction.closingTime").type(JsonFieldType.STRING).description("경매 마감시간"), - fieldWithPath("auction.directRegions").type(JsonFieldType.ARRAY).description("모든 직거래 지역"), - fieldWithPath("auction.directRegions.[]").type(JsonFieldType.ARRAY).description("단일 직거래 지역"), - fieldWithPath("auction.directRegions.[].first").type(JsonFieldType.STRING).description("첫 번째 직거래 지역"), - fieldWithPath("auction.directRegions.[].second").type(JsonFieldType.STRING).description("두 번째 직거래 지역"), - fieldWithPath("auction.directRegions.[].third").type(JsonFieldType.STRING).description("세 번째 직거래 지역"), - fieldWithPath("auction.auctioneerCount").type(JsonFieldType.NUMBER).description("경매 참여자 수"), - fieldWithPath("seller").type(JsonFieldType.OBJECT).description("판매자 정보"), - fieldWithPath("seller.id").type(JsonFieldType.NUMBER).description("판매자 ID"), - fieldWithPath("seller.image").type(JsonFieldType.STRING).description("판매자 프로필 이미지 주소"), - fieldWithPath("seller.nickname").type(JsonFieldType.STRING).description("판매자 닉네임"), - fieldWithPath("seller.reliability").type(JsonFieldType.NUMBER).description("판매자 신뢰도"), - fieldWithPath("chat.id").type(JsonFieldType.NUMBER).description("채팅방 ID"), - fieldWithPath("chat.isChatParticipant").type(JsonFieldType.BOOLEAN).description("채팅방을 생성 가능 유저 여부"), - fieldWithPath("isOwner").type(JsonFieldType.BOOLEAN).description("유저가 해당 경매 글을 작성한 유저인지에 대한 여부") - ) - ) - ); + final ResultActions resultActions + = mockMvc.perform(RestDocumentationRequestBuilders.get("/auctions/{auctionId}", 경매_조회_dto.id()) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, 유효한_액세스_토큰) + ) + .andExpectAll( + status().isOk(), + jsonPath("$.auction.id", is(경매_조회_dto.id()), Long.class), + jsonPath("$.auction.images").exists(), + jsonPath("$.auction.title", is(경매_조회_dto.title())), + jsonPath("$.auction.category.main", is(경매_조회_dto.mainCategory())), + jsonPath("$.auction.category.sub", is(경매_조회_dto.subCategory())), + jsonPath("$.auction.description", is(경매_조회_dto.description())), + jsonPath("$.auction.startPrice", is(경매_조회_dto.startPrice())), + jsonPath("$.auction.startPrice", is(경매_조회_dto.startPrice())), + jsonPath("$.auction.lastBidPrice", is(경매_조회_dto.lastBidPrice())), + jsonPath("$.auction.status", is(경매_조회_dto.auctionStatus().toString())), + jsonPath("$.auction.bidUnit", is(경매_조회_dto.bidUnit())), + jsonPath("$.auction.registerTime").exists(), + jsonPath("$.auction.closingTime").exists(), + jsonPath("$.auction.directRegions[0].first", is(경매_조회_dto.auctionRegions().get(0).firstRegionDto().regionName())), + jsonPath("$.auction.directRegions[0].second", is(경매_조회_dto.auctionRegions().get(0).secondRegionDto().regionName())), + jsonPath("$.auction.directRegions[0].third", is(경매_조회_dto.auctionRegions().get(0).thirdRegionDto().regionName())), + jsonPath("$.auction.auctioneerCount", is(경매_조회_dto.auctioneerCount())), + jsonPath("$.chat.id").exists(), + jsonPath("$.chat.isChatParticipant", is(true)), + jsonPath("$.isOwner", is(true)), + jsonPath("$.seller.nickname", is(경매_조회_dto.sellerName())), + jsonPath("$.seller.id", is(경매_조회_dto.sellerId()), Long.class), + jsonPath("$.seller.image", containsString(프로필_이미지_상대_주소)) + ); + + read_문서화(resultActions); } @Test void 경매_조회시_지정한_아이디에_해당하는_경매가_없다면_404를_반환한다() throws Exception { // given - final Long invalidAuctionId = -999L; - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(auctionService.readByAuctionId(anyLong(), any(AuthenticationUserInfo.class))) + given(auctionService.readByAuctionId(anyLong())) .willThrow(new AuctionNotFoundException("지정한 아이디에 대한 경매를 찾을 수 없습니다.")); - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); + given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(유효한_사용자_id_클레임)); // when & then - mockMvc.perform(get("/auctions/{auctionId}", invalidAuctionId) + mockMvc.perform(get("/auctions/{auctionId}", 존재하지_않는_경매_id) .contentType(MediaType.APPLICATION_JSON) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken")) + .header(HttpHeaders.AUTHORIZATION, 유효한_액세스_토큰)) .andExpectAll( status().isNotFound(), jsonPath("$.message").exists() @@ -585,137 +301,64 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { @Test void 첫번째_페이지의_경매_목록을_조회한다() throws Exception { // given - final ReadRegionsDto readRegionsDto = new ReadRegionsDto( - new ReadRegionDto(1L, "서울특별시"), - new ReadRegionDto(2L, "강서구"), - new ReadRegionDto(3L, "역삼동") - ); - final ReadAuctionDto auction1 = new ReadAuctionDto( - 1L, - "경매 상품 1", - "이것은 경매 상품 1 입니다.", - 1_000, - 1_000, - null, - false, - LocalDateTime.now(), - LocalDateTime.now(), - List.of(readRegionsDto), - List.of(1L), - 2, - "main1", - "sub1", - 1L, - "https://profile.com", - "판매자", - 3.5d - ); - final ReadAuctionDto auction2 = new ReadAuctionDto( - 2L, - "경매 상품 2", - "이것은 경매 상품 2 입니다.", - 1_000, - 1_000, - null, - false, - LocalDateTime.now(), - LocalDateTime.now(), - List.of(readRegionsDto), - List.of(1L), - 2, - "main2", - "sub2", - 1L, - "https://profile.com", - "판매자", - 3.5d - ); - - final ReadAuctionsDto readAuctionsDto = new ReadAuctionsDto(List.of(auction2, auction1), true); - given(auctionService.readAllByLastAuctionId(any(), anyInt())).willReturn(readAuctionsDto); + given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(유효한_사용자_id_클레임)); + given(auctionService.readAllByCondition(any(Pageable.class), any(ReadAuctionSearchCondition.class))) + .willReturn(경매_목록_조회_dto); // when & then - mockMvc.perform(get("/auctions") - .contentType(MediaType.APPLICATION_JSON) - .queryParam("size", "10") - ) - .andExpectAll( - status().isOk(), - jsonPath("$.auctions.[0].id", is(auction2.id()), Long.class), - jsonPath("$.auctions.[0].title", is(auction2.title())), - jsonPath("$.auctions.[0].image").exists(), - jsonPath("$.auctions.[0].auctionPrice", is(auction2.startPrice())), - jsonPath("$.auctions.[0].status").exists(), - jsonPath("$.auctions.[0].auctioneerCount", is(auction2.auctioneerCount())), - jsonPath("$.auctions.[1].id", is(auction1.id()), Long.class), - jsonPath("$.auctions.[1].title", is(auction1.title())), - jsonPath("$.auctions.[1].image").exists(), - jsonPath("$.auctions.[1].auctionPrice", is(auction1.startPrice())), - jsonPath("$.auctions.[1].status").exists(), - jsonPath("$.auctions.[1].auctioneerCount", is(auction1.auctioneerCount())) - ) - .andDo( - restDocs.document( - queryParameters( - parameterWithName("lastAuctionId").description("마지막으로 조회한 경매 ID").optional(), - parameterWithName("size").description("페이지 크기").optional() - ), - responseFields( - fieldWithPath("auctions").type(JsonFieldType.ARRAY).description("조회한 경매 목록"), - fieldWithPath("auctions.[]").type(JsonFieldType.ARRAY).description("조회한 단일 경매 정보"), - fieldWithPath("auctions.[].id").type(JsonFieldType.NUMBER).description("경매 ID"), - fieldWithPath("auctions.[].title").type(JsonFieldType.STRING).description("경매 글 제목"), - fieldWithPath("auctions.[].image").type(JsonFieldType.STRING).description("경매 대표 이미지"), - fieldWithPath("auctions.[].auctionPrice").type(JsonFieldType.NUMBER).description("경매가(시작가, 현재가, 낙찰가 중 하나)"), - fieldWithPath("auctions.[].status").type(JsonFieldType.STRING).description("경매 상태"), - fieldWithPath("auctions.[].auctioneerCount").type(JsonFieldType.NUMBER).description("경매 참여자 수"), - fieldWithPath("isLast").type(JsonFieldType.BOOLEAN).description("마지막 페이지 여부") - ) - ) - ); + final ResultActions resultActions = mockMvc.perform(get("/auctions") + .contentType(MediaType.APPLICATION_JSON) + .queryParam("size", "10") + .header(HttpHeaders.AUTHORIZATION, 유효한_액세스_토큰) + ) + .andExpectAll( + status().isOk(), + jsonPath("$.auctions.[0].id", is(두번째_경매_조회_dto.id()), Long.class), + jsonPath("$.auctions.[0].title", is(두번째_경매_조회_dto.title())), + jsonPath("$.auctions.[0].image", containsString(경매_이미지_상대_주소)), + jsonPath("$.auctions.[0].auctionPrice", is(두번째_경매_조회_dto.startPrice())), + jsonPath("$.auctions.[0].status").exists(), + jsonPath("$.auctions.[0].auctioneerCount", is(두번째_경매_조회_dto.auctioneerCount())), + jsonPath("$.auctions.[1].id", is(첫번째_경매_조회_dto.id()), Long.class), + jsonPath("$.auctions.[1].title", is(첫번째_경매_조회_dto.title())), + jsonPath("$.auctions.[1].image", containsString(경매_이미지_상대_주소)), + jsonPath("$.auctions.[1].auctionPrice", is(첫번째_경매_조회_dto.startPrice())), + jsonPath("$.auctions.[1].status").exists(), + jsonPath("$.auctions.[1].auctioneerCount", is(첫번째_경매_조회_dto.auctioneerCount())), + jsonPath("$.isLast").exists() + ); + + readAllByCondition_문서화(resultActions); } @Test void 지정한_아이디에_해당하는_경매를_삭제한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); + given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(유효한_사용자_id_클레임)); willDoNothing().given(auctionService).deleteByAuctionId(anyLong(), anyLong()); // when & then - mockMvc.perform(RestDocumentationRequestBuilders - .delete("/auctions/{auctionId}", 1L) - .contentType(MediaType.APPLICATION_JSON) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - ) - .andExpectAll(status().isNoContent()) - .andDo( - restDocs.document( - requestHeaders( - headerWithName("Authorization").description("회원 Bearer 인증 정보") - ), - pathParameters( - parameterWithName("auctionId").description("삭제할 경매 ID") - ) - ) - ); + final ResultActions resultActions + = mockMvc.perform(RestDocumentationRequestBuilders.delete("/auctions/{auctionId}", 유효한_경매_id) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, 유효한_액세스_토큰) + ) + .andExpectAll(status().isNoContent()); + + delete_문서화(resultActions); } @Test void 경매_삭제시_지정한_아이디에_해당하는_경매가_없다면_404를_반환한다() throws Exception { // given - final Long invalidAuctionId = -999L; - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); + given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(유효한_사용자_id_클레임)); willThrow(new AuctionNotFoundException("지정한 아이디에 대한 경매를 찾을 수 없습니다.")) .given(auctionService).deleteByAuctionId(anyLong(), anyLong()); // when & then - mockMvc.perform(delete("/auctions/{auctionId}", invalidAuctionId) + mockMvc.perform(delete("/auctions/{auctionId}", 존재하지_않는_경매_id) .contentType(MediaType.APPLICATION_JSON) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 유효한_액세스_토큰) ) .andExpectAll( status().isNotFound(), @@ -726,21 +369,144 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { @Test void 경매_삭제시_유효한_회원이_아니라면_404를_반환한다() throws Exception { // given - final Long invalidAuctionId = -999L; - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); + given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(유효한_사용자_id_클레임)); willThrow(new UserNotFoundException("회원 정보를 찾을 수 없습니다.")) .given(auctionService).deleteByAuctionId(anyLong(), anyLong()); // when & then - mockMvc.perform(delete("/auctions/{auctionId}", invalidAuctionId) + mockMvc.perform(delete("/auctions/{auctionId}", 존재하지_않는_경매_id) .contentType(MediaType.APPLICATION_JSON) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 유효한_액세스_토큰) ) .andExpectAll( status().isNotFound(), jsonPath("$.message").exists() ); } + + private void create_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + requestParts( + partWithName("images").description("이미지 파일(Array, 최대 10장)"), + partWithName("request").description("요청 데이터") + ), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("경매 ID"), + fieldWithPath("title").type(JsonFieldType.STRING).description("경매 글 제목"), + fieldWithPath("image").type(JsonFieldType.STRING).description("경매 대표 이미지"), + fieldWithPath("auctionPrice").type(JsonFieldType.NUMBER).description("시작가"), + fieldWithPath("status").type(JsonFieldType.STRING).description("경매 상태"), + fieldWithPath("auctioneerCount").type(JsonFieldType.NUMBER).description("경매 참여자 수") + ) + ) + ); + } + + private void read_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + pathParameters( + parameterWithName("auctionId").description("조회하고자 하는 경매 ID") + ), + responseFields( + fieldWithPath("auction.id").type(JsonFieldType.NUMBER).description("경매 글 ID"), + fieldWithPath("auction.images").type(JsonFieldType.ARRAY).description("경매 이미지"), + fieldWithPath("auction.title").type(JsonFieldType.STRING).description("경매 글 제목"), + fieldWithPath("auction.category").type(JsonFieldType.OBJECT) + .description("경매 카테고리"), + fieldWithPath("auction.category.main").type(JsonFieldType.STRING) + .description("상위 카테고리"), + fieldWithPath("auction.category.sub").type(JsonFieldType.STRING) + .description("하위 카테고리"), + fieldWithPath("auction.description").type(JsonFieldType.STRING) + .description("경매 본문"), + fieldWithPath("auction.startPrice").type(JsonFieldType.NUMBER) + .description("시작가"), + fieldWithPath("auction.lastBidPrice").description("마지막 입찰가"), + fieldWithPath("auction.status").description("경매 상태"), + fieldWithPath("auction.bidUnit").type(JsonFieldType.NUMBER).description("입찰 단위"), + fieldWithPath("auction.registerTime").type(JsonFieldType.STRING) + .description("경매 등록시간"), + fieldWithPath("auction.closingTime").type(JsonFieldType.STRING) + .description("경매 마감시간"), + fieldWithPath("auction.directRegions").type(JsonFieldType.ARRAY) + .description("모든 직거래 지역"), + fieldWithPath("auction.directRegions.[]").type(JsonFieldType.ARRAY) + .description("단일 직거래 지역"), + fieldWithPath("auction.directRegions.[].first").type(JsonFieldType.STRING) + .description("첫 번째 직거래 지역"), + fieldWithPath("auction.directRegions.[].second").type(JsonFieldType.STRING) + .description("두 번째 직거래 지역"), + fieldWithPath("auction.directRegions.[].third").type(JsonFieldType.STRING) + .description("세 번째 직거래 지역"), + fieldWithPath("auction.auctioneerCount").type(JsonFieldType.NUMBER) + .description("경매 참여자 수"), + fieldWithPath("seller").type(JsonFieldType.OBJECT).description("판매자 정보"), + fieldWithPath("seller.id").type(JsonFieldType.NUMBER).description("판매자 ID"), + fieldWithPath("seller.image").type(JsonFieldType.STRING) + .description("판매자 프로필 이미지 주소"), + fieldWithPath("seller.nickname").type(JsonFieldType.STRING) + .description("판매자 닉네임"), + fieldWithPath("seller.reliability").type(JsonFieldType.NUMBER) + .description("판매자 신뢰도"), + fieldWithPath("chat.id").type(JsonFieldType.NUMBER).description("채팅방 ID"), + fieldWithPath("chat.isChatParticipant").type(JsonFieldType.BOOLEAN) + .description("채팅방을 생성 가능 유저 여부"), + fieldWithPath("isOwner").type(JsonFieldType.BOOLEAN) + .description("유저가 해당 경매 글을 작성한 유저인지에 대한 여부") + ) + ) + ); + } + + private void readAllByCondition_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + queryParameters( + parameterWithName("lastAuctionId").description("마지막으로 조회한 경매 ID").optional(), + parameterWithName("size").description("페이지 크기").optional() + ), + responseFields( + fieldWithPath("auctions").type(JsonFieldType.ARRAY).description("조회한 경매 목록"), + fieldWithPath("auctions.[]").type(JsonFieldType.ARRAY) + .description("조회한 단일 경매 정보"), + fieldWithPath("auctions.[].id").type(JsonFieldType.NUMBER).description("경매 ID"), + fieldWithPath("auctions.[].title").type(JsonFieldType.STRING) + .description("경매 글 제목"), + fieldWithPath("auctions.[].image").type(JsonFieldType.STRING) + .description("경매 대표 이미지"), + fieldWithPath("auctions.[].auctionPrice").type(JsonFieldType.NUMBER) + .description("경매가(시작가, 현재가, 낙찰가 중 하나)"), + fieldWithPath("auctions.[].status").type(JsonFieldType.STRING) + .description("경매 상태"), + fieldWithPath("auctions.[].auctioneerCount").type(JsonFieldType.NUMBER) + .description("경매 참여자 수"), + fieldWithPath("isLast").type(JsonFieldType.BOOLEAN).description("마지막 페이지 여부") + ) + ) + ); + } + + private void delete_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + pathParameters( + parameterWithName("auctionId").description("삭제할 경매 ID") + ) + ) + ); + } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionQnaControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionQnaControllerTest.java new file mode 100644 index 000000000..ca0ab7c8c --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionQnaControllerTest.java @@ -0,0 +1,169 @@ +package com.ddang.ddang.auction.presentation; + +import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; +import com.ddang.ddang.auction.presentation.fixture.AuctionQuestionControllerFixture; +import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; +import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; +import com.ddang.ddang.authentication.domain.TokenDecoder; +import com.ddang.ddang.authentication.domain.TokenType; +import com.ddang.ddang.authentication.domain.dto.AuthenticationStore; +import com.ddang.ddang.exception.GlobalExceptionHandler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.Optional; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SuppressWarnings("NonAsciiCharacters") +class AuctionQnaControllerTest extends AuctionQuestionControllerFixture { + + TokenDecoder tokenDecoder; + + MockMvc mockMvc; + + @BeforeEach + void setUp() { + tokenDecoder = mock(TokenDecoder.class); + + final AuthenticationStore store = new AuthenticationStore(); + final AuthenticationInterceptor interceptor = new AuthenticationInterceptor( + blackListTokenService, + authenticationUserService, + tokenDecoder, + store + ); + final AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(store); + + mockMvc = MockMvcBuilders.standaloneSetup(auctionQnaController) + .setControllerAdvice(new GlobalExceptionHandler()) + .addInterceptors(interceptor) + .setCustomArgumentResolvers(resolver) + .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) + .alwaysDo(print()) + .alwaysDo(restDocs) + .build(); + } + + @Test + void 경매_아이디를_통해_질문과_답변을_모두_조회한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(questionService.readAllByAuctionId(anyLong())).willReturn(질문과_답변_정보들_dto); + + // when & then + final ResultActions resultActions = + mockMvc.perform(get("/auctions/{auctionId}/questions", 조회할_경매_아이디) + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpectAll( + status().isOk(), + jsonPath("$.qnas.[0].question.id", is(질문_정보_dto1.id()), Long.class), + jsonPath("$.qnas.[0].question.writer.id", is(질문자_정보_dto.id()), Long.class), + jsonPath("$.qnas.[0].question.writer.name", is(질문자_정보_dto.name())), + jsonPath("$.qnas.[0].question.writer.image").exists(), + jsonPath("$.qnas.[0].question.createdTime").exists(), + jsonPath("$.qnas.[0].question.content", is(질문_정보_dto1.content())), + jsonPath("$.qnas.[0].answer.id", is(답변_정보_dto1.id()), Long.class), + jsonPath("$.qnas.[0].answer.writer.id", is(판매자_정보_dto.id()), Long.class), + jsonPath("$.qnas.[0].answer.writer.name", is(판매자_정보_dto.name())), + jsonPath("$.qnas.[0].answer.writer.image").exists(), + jsonPath("$.qnas.[0].answer.createdTime").exists(), + jsonPath("$.qnas.[0].answer.content", is(답변_정보_dto1.content())), + jsonPath("$.qnas.[1].question.id", is(질문_정보_dto2.id()), Long.class), + jsonPath("$.qnas.[1].question.writer.id", is(질문자_정보_dto.id()), Long.class), + jsonPath("$.qnas.[1].question.writer.name", is(질문자_정보_dto.name())), + jsonPath("$.qnas.[1].question.writer.image").exists(), + jsonPath("$.qnas.[1].question.createdTime").exists(), + jsonPath("$.qnas.[1].question.content", is(질문_정보_dto2.content())), + jsonPath("$.qnas.[1].answer.id", is(답변_정보_dto2.id()), Long.class), + jsonPath("$.qnas.[1].answer.writer.id", is(판매자_정보_dto.id()), Long.class), + jsonPath("$.qnas.[1].answer.writer.name", is(판매자_정보_dto.name())), + jsonPath("$.qnas.[1].answer.writer.image").exists(), + jsonPath("$.qnas.[1].answer.createdTime").exists(), + jsonPath("$.qnas.[1].answer.content", is(답변_정보_dto2.content())) + ); + + readAllByAuctionId_문서화(resultActions); + } + + @Test + void 존재하지_않는_경매_아이디를_통해_질문과_답변을_모두_조회할시_404를_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(questionService.readAllByAuctionId(anyLong())) + .willThrow(new AuctionNotFoundException("해당 경매를 찾을 수 없습니다.")); + + // when & then + mockMvc.perform(get("/auctions/{auctionId}/questions", 조회할_경매_아이디) + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpectAll( + status().isNotFound(), + jsonPath("$.message").exists() + ); + } + + private void readAllByAuctionId_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + responseFields( + fieldWithPath("qnas.[]").type(JsonFieldType.ARRAY) + .description("모든 경매 질문과 답변 목록"), + fieldWithPath("qnas.[].question").type(JsonFieldType.OBJECT) + .description("질문 정보 JSON"), + fieldWithPath("qnas.[].question.id").type(JsonFieldType.NUMBER) + .description("경매 질문 글 ID"), + fieldWithPath("qnas.[].question.writer").type(JsonFieldType.OBJECT) + .description("질문자 정보 JSON"), + fieldWithPath("qnas.[].question.writer.id").type(JsonFieldType.NUMBER) + .description("질문자의 ID"), + fieldWithPath("qnas.[].question.writer.name").type(JsonFieldType.STRING) + .description("질문자의 이름"), + fieldWithPath("qnas.[].question.writer.image").type(JsonFieldType.STRING) + .description("질문자의 프로필 이미지 URL"), + fieldWithPath("qnas.[].question.createdTime").type(JsonFieldType.STRING) + .description("질문 등록 시간"), + fieldWithPath("qnas.[].question.content").type(JsonFieldType.STRING) + .description("질문 내용"), + fieldWithPath("qnas.[].answer").type(JsonFieldType.OBJECT) + .description("답변 정보 JSON"), + fieldWithPath("qnas.[].answer.id").type(JsonFieldType.NUMBER) + .description("경매 답변 글 ID"), + fieldWithPath("qnas.[].answer.writer").type(JsonFieldType.OBJECT) + .description("답변자 정보 JSON"), + fieldWithPath("qnas.[].answer.writer.id").type(JsonFieldType.NUMBER) + .description("답변자의 ID"), + fieldWithPath("qnas.[].answer.writer.name").type(JsonFieldType.STRING) + .description("답변자의 이름"), + fieldWithPath("qnas.[].answer.writer.image").type(JsonFieldType.STRING) + .description("답변자의 프로필 이미지 URL"), + fieldWithPath("qnas.[].answer.createdTime").type(JsonFieldType.STRING) + .description("답변 등록 시간"), + fieldWithPath("qnas.[].answer.content").type(JsonFieldType.STRING) + .description("답변 내용") + ) + ) + ); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionReviewControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionReviewControllerTest.java new file mode 100644 index 000000000..d6e558ff3 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionReviewControllerTest.java @@ -0,0 +1,110 @@ +package com.ddang.ddang.auction.presentation; + +import com.ddang.ddang.auction.presentation.fixture.AuctionReviewControllerFixture; +import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; +import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; +import com.ddang.ddang.authentication.domain.TokenDecoder; +import com.ddang.ddang.authentication.domain.TokenType; +import com.ddang.ddang.authentication.domain.dto.AuthenticationStore; +import com.ddang.ddang.exception.GlobalExceptionHandler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.Optional; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SuppressWarnings("NonAsciiCharacters") +class AuctionReviewControllerTest extends AuctionReviewControllerFixture { + + TokenDecoder tokenDecoder; + MockMvc mockMvc; + + @BeforeEach + void setUp() { + tokenDecoder = mock(TokenDecoder.class); + + final AuthenticationStore store = new AuthenticationStore(); + final AuthenticationInterceptor interceptor = new AuthenticationInterceptor( + blackListTokenService, + authenticationUserService, + tokenDecoder, + store + ); + final AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(store); + + mockMvc = MockMvcBuilders.standaloneSetup(auctionReviewController) + .setControllerAdvice(new GlobalExceptionHandler()) + .addInterceptors(interceptor) + .setCustomArgumentResolvers(resolver) + .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) + .alwaysDo(print()) + .alwaysDo(restDocs) + .build(); + } + + @Test + void 사용자가_경매_거래에_작성한_평가를_조회한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(유효한_작성자_비공개_클레임)); + given(reviewService.readByAuctionIdAndWriterId(anyLong(), anyLong())) + .willReturn(구매자가_판매자1에게_받은_평가_내용); + + // when & then + final ResultActions resultActions = + mockMvc.perform(RestDocumentationRequestBuilders.get("/auctions/{auctionId}/reviews", 유효한_경매_아이디) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpectAll( + status().isOk(), + jsonPath("score", is(구매자가_판매자1에게_받은_평가_내용.score()), Double.class), + jsonPath("content", is(구매자가_판매자1에게_받은_평가_내용.content())) + ); + + readByAuctionId_문서화(resultActions); + } + + + + private void readByAuctionId_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("회원 Bearer 인증 정보") + ), + pathParameters( + parameterWithName("auctionId").description("평가와_관련된_경매_아이디") + ), + responseFields( + fieldWithPath("score").type(JsonFieldType.NUMBER) + .description("평가 점수"), + fieldWithPath("content").type(JsonFieldType.STRING) + .description("평가 내용") + ) + ) + ); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/fixture/AuctionControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/fixture/AuctionControllerFixture.java new file mode 100644 index 000000000..804f0e3ec --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/fixture/AuctionControllerFixture.java @@ -0,0 +1,211 @@ +package com.ddang.ddang.auction.presentation.fixture; + +import com.ddang.ddang.auction.application.dto.CreateInfoAuctionDto; +import com.ddang.ddang.auction.application.dto.ReadAuctionDto; +import com.ddang.ddang.auction.application.dto.ReadAuctionsDto; +import com.ddang.ddang.auction.application.dto.ReadChatRoomDto; +import com.ddang.ddang.auction.application.dto.ReadRegionDto; +import com.ddang.ddang.auction.application.dto.ReadRegionsDto; +import com.ddang.ddang.auction.domain.AuctionStatus; +import com.ddang.ddang.auction.presentation.dto.request.CreateAuctionRequest; +import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims; +import com.ddang.ddang.configuration.CommonControllerSliceTest; +import com.fasterxml.jackson.core.JsonProcessingException; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; + +@SuppressWarnings("NonAsciiCharacters") +public class AuctionControllerFixture extends CommonControllerSliceTest { + + protected Long 존재하지_않는_경매_id = -999L; + protected Long 유효한_경매_id = 1L; + protected String 경매_이미지_상대_주소 = "/auctions/images"; + protected String 프로필_이미지_상대_주소 = "/users/images"; + protected String 경매_생성_시_경매_상태 = "UNBIDDEN"; + protected int 경매_생성_시_경매_참여자_수 = 0; + protected String 유효한_액세스_토큰 = "Bearer accessToken"; + protected MockMultipartFile 유효한_경매_이미지_파일; + protected MockMultipartFile 비어_있는_경매_이미지_파일; + protected MockMultipartFile 유효하지_않은_확장자_경매_이미지_파일; + protected MockMultipartFile 유효한_경매_등록_request_multipartFile; + protected MockMultipartFile 유효하지_않은_카테고리_경매_등록_request_multipartFile; + protected MockMultipartFile 비어있는_경매_이미지_경매_등록_request_multipartFile; + protected MockMultipartFile 유효하지_않은_지역_경매_등록_request_multipartFile; + protected PrivateClaims 유효한_사용자_id_클레임 = new PrivateClaims(1L); + protected PrivateClaims 유효하지_않은_사용자_id_클레임 = new PrivateClaims(-999L); + protected CreateInfoAuctionDto 경매_등록_결과_dto = new CreateInfoAuctionDto( + 1L, + "제목", + 1L, + 1_000 + ); + protected ReadChatRoomDto 쪽지방_dto = new ReadChatRoomDto(1L, true); + protected ReadAuctionDto 경매_조회_dto; + protected ReadAuctionDto 첫번째_경매_조회_dto; + protected ReadAuctionDto 두번째_경매_조회_dto; + protected ReadAuctionsDto 경매_목록_조회_dto; + + + private CreateAuctionRequest 유효한_경매_등록_request = new CreateAuctionRequest( + "제목", + "내용", + 1_000, + 1_000, + LocalDateTime.now().plusDays(3L), + 2L, + List.of(3L) + ); + private CreateAuctionRequest 유효하지_않은_카테고리_경매_등록_request = new CreateAuctionRequest( + "제목", + "내용", + 1_000, + 1_000, + LocalDateTime.now().plusDays(3L), + -999L, + List.of(3L) + ); + private CreateAuctionRequest 비어있는_경매_이미지_경매_등록_request = new CreateAuctionRequest( + "제목", + "내용", + 1_000, + 1_000, + LocalDateTime.now().plusDays(3L), + -999L, + List.of(3L) + ); + private CreateAuctionRequest 유효하지_않은_지역_경매_등록_request = new CreateAuctionRequest( + "제목", + "내용", + 1_000, + 1_000, + LocalDateTime.now().plusDays(3L), + -999L, + List.of(3L) + ); + + @BeforeEach + void fixtureSetUp() throws JsonProcessingException { + 유효한_경매_이미지_파일 = new MockMultipartFile( + "images", + "image.png", + MediaType.IMAGE_PNG_VALUE, + new byte[]{1} + ); + 비어_있는_경매_이미지_파일 = new MockMultipartFile( + "images", + "image.png", + MediaType.IMAGE_PNG_VALUE, + new byte[0] + ); + 유효하지_않은_확장자_경매_이미지_파일 = new MockMultipartFile( + "images", + "image.exe", + MediaType.IMAGE_PNG_VALUE, + new byte[]{1} + ); + 유효한_경매_등록_request_multipartFile = new MockMultipartFile( + "request", + "request", + MediaType.APPLICATION_JSON_VALUE, + objectMapper.writeValueAsBytes(유효한_경매_등록_request) + ); + 유효하지_않은_카테고리_경매_등록_request_multipartFile = new MockMultipartFile( + "request", + "request", + MediaType.APPLICATION_JSON_VALUE, + objectMapper.writeValueAsBytes(유효하지_않은_카테고리_경매_등록_request) + ); + 비어있는_경매_이미지_경매_등록_request_multipartFile = new MockMultipartFile( + "request", + "request", + MediaType.APPLICATION_JSON_VALUE, + objectMapper.writeValueAsBytes(비어있는_경매_이미지_경매_등록_request) + ); + 유효하지_않은_지역_경매_등록_request_multipartFile = new MockMultipartFile( + "request", + "request", + MediaType.APPLICATION_JSON_VALUE, + objectMapper.writeValueAsBytes(유효하지_않은_지역_경매_등록_request) + ); + + final ReadRegionsDto 서울특별시_강서구_역삼동 = new ReadRegionsDto( + new ReadRegionDto(1L, "서울특별시"), + new ReadRegionDto(2L, "강서구"), + new ReadRegionDto(3L, "역삼동") + ); + + 경매_조회_dto = new ReadAuctionDto( + 1L, + "경매 상품 1", + "이것은 경매 상품 1 입니다.", + 1_000, + 1_000, + null, + false, + LocalDateTime.now(), + LocalDateTime.now(), + List.of(서울특별시_강서구_역삼동), + List.of(1L), + 2, + "main", + "sub", + 1L, + 1L, + "판매자", + 3.5d, + false, + AuctionStatus.UNBIDDEN + ); + + 첫번째_경매_조회_dto = new ReadAuctionDto( + 1L, + "경매 상품 1", + "이것은 경매 상품 1 입니다.", + 1_000, + 1_000, + null, + false, + LocalDateTime.now(), + LocalDateTime.now(), + List.of(서울특별시_강서구_역삼동), + List.of(1L), + 2, + "main", + "sub", + 1L, + 1L, + "판매자", + 3.5d, + false, + AuctionStatus.UNBIDDEN + ); + + 두번째_경매_조회_dto = new ReadAuctionDto( + 2L, + "경매 상품 1", + "이것은 경매 상품 1 입니다.", + 1_000, + 1_000, + null, + false, + LocalDateTime.now(), + LocalDateTime.now(), + List.of(서울특별시_강서구_역삼동), + List.of(1L), + 2, + "main", + "sub", + 1L, + 1L, + "판매자", + 3.5d, + false, + AuctionStatus.UNBIDDEN + ); + + 경매_목록_조회_dto = new ReadAuctionsDto(List.of(두번째_경매_조회_dto, 첫번째_경매_조회_dto), true); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/fixture/AuctionQuestionControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/fixture/AuctionQuestionControllerFixture.java new file mode 100644 index 000000000..2ee6bd63c --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/fixture/AuctionQuestionControllerFixture.java @@ -0,0 +1,45 @@ +package com.ddang.ddang.auction.presentation.fixture; + +import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims; +import com.ddang.ddang.configuration.CommonControllerSliceTest; +import com.ddang.ddang.qna.application.dto.ReadAnswerDto; +import com.ddang.ddang.qna.application.dto.ReadQnaDto; +import com.ddang.ddang.qna.application.dto.ReadQnasDto; +import com.ddang.ddang.qna.application.dto.ReadQuestionDto; +import com.ddang.ddang.qna.application.dto.ReadUserInQnaDto; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class AuctionQuestionControllerFixture extends CommonControllerSliceTest { + + protected String 엑세스_토큰_값 = "Bearer accessToken"; + protected PrivateClaims 사용자_ID_클레임 = new PrivateClaims(1L); + protected Long 조회할_경매_아이디 = 1L; + + protected ReadUserInQnaDto 질문자_정보_dto = new ReadUserInQnaDto( + 1L, + "질문자", + 1L, + 4.5d, + "12345", + false + ); + protected ReadUserInQnaDto 판매자_정보_dto = new ReadUserInQnaDto( + 2L, + "판매자", + 2L, + 4.5d, + "12346", + false + ); + protected ReadQuestionDto 질문_정보_dto1 = new ReadQuestionDto(1L, 질문자_정보_dto, "질문1", LocalDateTime.now()); + protected ReadQuestionDto 질문_정보_dto2 = new ReadQuestionDto(2L, 질문자_정보_dto, "질문2", LocalDateTime.now()); + protected ReadAnswerDto 답변_정보_dto1 = new ReadAnswerDto(1L, 판매자_정보_dto, "답변1", LocalDateTime.now()); + protected ReadAnswerDto 답변_정보_dto2 = new ReadAnswerDto(2L, 판매자_정보_dto, "답변1", LocalDateTime.now()); + private ReadQnaDto 질문과_답변_정보_dto1 = new ReadQnaDto(질문_정보_dto1, 답변_정보_dto1); + private ReadQnaDto 질문과_답변_정보_dto2 = new ReadQnaDto(질문_정보_dto2, 답변_정보_dto2); + protected ReadQnasDto 질문과_답변_정보들_dto = + new ReadQnasDto(List.of(질문과_답변_정보_dto1, 질문과_답변_정보_dto2)); +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/fixture/AuctionReviewControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/fixture/AuctionReviewControllerFixture.java new file mode 100644 index 000000000..b0fbbf4f8 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/fixture/AuctionReviewControllerFixture.java @@ -0,0 +1,27 @@ +package com.ddang.ddang.auction.presentation.fixture; + +import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims; +import com.ddang.ddang.configuration.CommonControllerSliceTest; +import com.ddang.ddang.review.application.dto.ReadReviewDetailDto; +import com.ddang.ddang.review.application.dto.ReadReviewDto; +import com.ddang.ddang.review.application.dto.ReadUserInReviewDto; + +import java.time.LocalDateTime; + +@SuppressWarnings("NonAsciiCharacters") +public class AuctionReviewControllerFixture extends CommonControllerSliceTest { + + protected Long 유효한_평가_작성자_아이디 = 1L; + protected String 액세스_토큰 = "Bearer accessToken"; + protected PrivateClaims 유효한_작성자_비공개_클레임 = new PrivateClaims(유효한_평가_작성자_아이디); + protected Long 유효한_경매_아이디 = 1L; + protected Long 판매자_프로필_이미지_아이디 = 1L; + protected ReadUserInReviewDto 판매자 = new ReadUserInReviewDto(1L, "판매자", 판매자_프로필_이미지_아이디, 5.0d, "12347"); + protected Long 구매자가_판매자에게_받은_평가_아이디 = 1L; + protected ReadReviewDto 구매자가_판매자1에게_받은_평가 = + new ReadReviewDto(구매자가_판매자에게_받은_평가_아이디, 판매자, "친절하다.", 5.0d, LocalDateTime.now()); + protected ReadReviewDetailDto 구매자가_판매자1에게_받은_평가_내용 = new ReadReviewDetailDto( + 구매자가_판매자1에게_받은_평가.score(), + 구매자가_판매자1에게_받은_평가.content() + ); +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/authentication/application/AuthenticationServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/authentication/application/AuthenticationServiceTest.java index 3f7c3c214..6238cad68 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/authentication/application/AuthenticationServiceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/authentication/application/AuthenticationServiceTest.java @@ -4,27 +4,22 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; import com.ddang.ddang.authentication.application.dto.TokenDto; -import com.ddang.ddang.authentication.domain.exception.InvalidTokenException; +import com.ddang.ddang.authentication.application.exception.InvalidWithdrawalException; +import com.ddang.ddang.authentication.application.fixture.AuthenticationServiceFixture; import com.ddang.ddang.authentication.domain.Oauth2UserInformationProviderComposite; import com.ddang.ddang.authentication.domain.TokenDecoder; import com.ddang.ddang.authentication.domain.TokenEncoder; -import com.ddang.ddang.authentication.domain.TokenType; -import com.ddang.ddang.authentication.domain.dto.UserInformationDto; +import com.ddang.ddang.authentication.domain.exception.InvalidTokenException; import com.ddang.ddang.authentication.domain.exception.UnsupportedSocialLoginException; -import com.ddang.ddang.authentication.infrastructure.jwt.JwtEncoder; import com.ddang.ddang.authentication.infrastructure.oauth2.OAuth2UserInformationProvider; -import com.ddang.ddang.authentication.infrastructure.oauth2.Oauth2Type; import com.ddang.ddang.configuration.IsolateDatabase; -import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.device.application.DeviceTokenService; +import com.ddang.ddang.device.infrastructure.persistence.JpaDeviceTokenRepository; +import com.ddang.ddang.image.application.exception.ImageNotFoundException; +import com.ddang.ddang.image.infrastructure.persistence.JpaProfileImageRepository; import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; -import java.time.Clock; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.Map; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; @@ -32,54 +27,78 @@ import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; @IsolateDatabase @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class AuthenticationServiceTest { +class AuthenticationServiceTest extends AuthenticationServiceFixture { - AuthenticationService authenticationService; + @Mock + JpaProfileImageRepository defaultProfileImageRepository; + + @MockBean + Oauth2UserInformationProviderComposite providerComposite; - OAuth2UserInformationProvider mockProvider; + @MockBean + OAuth2UserInformationProvider userInfoProvider; - Oauth2UserInformationProviderComposite mockProviderComposite; + @MockBean + DeviceTokenService deviceTokenService; @Autowired JpaUserRepository userRepository; + @Autowired + JpaProfileImageRepository profileImageRepository; + @Autowired TokenEncoder tokenEncoder; @Autowired - JwtEncoder jwtEncoder; + TokenDecoder tokenDecoder; - @Mock - Clock clock; + @Autowired + BlackListTokenService blackListTokenService; + + @Autowired + JpaDeviceTokenRepository deviceTokenRepository; + + AuthenticationService authenticationService; + AuthenticationService profileImageNotFoundAuthenticationService; @BeforeEach - void setUp( - @Autowired JpaUserRepository userRepository, - @Autowired TokenEncoder tokenEncoder, - @Autowired TokenDecoder tokenDecoder - ) { - mockProvider = mock(OAuth2UserInformationProvider.class); - mockProviderComposite = mock(Oauth2UserInformationProviderComposite.class); + void setUp() { authenticationService = new AuthenticationService( - mockProviderComposite, + deviceTokenService, + providerComposite, + userRepository, + profileImageRepository, + tokenEncoder, + tokenDecoder, + blackListTokenService, + deviceTokenRepository + ); + profileImageNotFoundAuthenticationService = new AuthenticationService( + deviceTokenService, + providerComposite, userRepository, + defaultProfileImageRepository, tokenEncoder, - tokenDecoder + tokenDecoder, + blackListTokenService, + deviceTokenRepository ); } @Test void 지원하는_소셜_로그인_기능이_아닌_경우_예외가_발생한다() { // given - given(mockProviderComposite.findProvider(Oauth2Type.KAKAO)) + given(providerComposite.findProvider(지원하지_않는_소셜_로그인_타입)) .willThrow(new UnsupportedSocialLoginException("지원하는 소셜 로그인 기능이 아닙니다.")); // when & then - assertThatThrownBy(() -> authenticationService.login(Oauth2Type.KAKAO, "accessToken")) + assertThatThrownBy(() -> authenticationService.login(지원하지_않는_소셜_로그인_타입, 유효한_소셜_로그인_토큰, 디바이스_토큰)) .isInstanceOf(UnsupportedSocialLoginException.class) .hasMessage("지원하는 소셜 로그인 기능이 아닙니다."); } @@ -87,14 +106,11 @@ void setUp( @Test void 권한이_없는_소셜_로그인_토큰을_전달하면_예외가_발생한다() { // given - given(mockProviderComposite.findProvider(Oauth2Type.KAKAO)).willReturn(mockProvider); - given(mockProvider.findUserInformation(anyString())) - .willThrow(new InvalidTokenException("401 Unauthorized")); - - final String invalidAccessToken = "invalidAccessToken"; + given(providerComposite.findProvider(지원하는_소셜_로그인_타입)).willReturn(userInfoProvider); + given(userInfoProvider.findUserInformation(anyString())).willThrow(new InvalidTokenException("401 Unauthorized")); // when & then - assertThatThrownBy(() -> authenticationService.login(Oauth2Type.KAKAO, invalidAccessToken)) + assertThatThrownBy(() -> authenticationService.login(지원하지_않는_소셜_로그인_타입, 유효한_소셜_로그인_토큰, 디바이스_토큰)) .isInstanceOf(InvalidTokenException.class) .hasMessage("401 Unauthorized"); } @@ -102,22 +118,39 @@ void setUp( @Test void 가입한_회원이_소셜_로그인을_할_경우_accessToken과_refreshToken을_반환한다() { // given - final User user = User.builder() - .name("kakao12345") - .profileImage("프로필") - .reliability(0.0d) - .oauthId("12345") - .build(); + given(providerComposite.findProvider(지원하는_소셜_로그인_타입)).willReturn(userInfoProvider); + given(userInfoProvider.findUserInformation(anyString())).willReturn(사용자_회원_정보); - userRepository.save(user); + // when + final TokenDto actual = authenticationService.login(지원하는_소셜_로그인_타입, 유효한_소셜_로그인_토큰, 디바이스_토큰); - final UserInformationDto userInformationDto = new UserInformationDto(12345L); + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual.accessToken()).isNotEmpty().contains("Bearer "); + softAssertions.assertThat(actual.refreshToken()).isNotEmpty().contains("Bearer "); + }); + } + + @Test + void 가입하지_않은_회원이_소셜_로그인을_할_때_기본_프로필_이미지를_찾을_수_없으면_예외가_발생한다() { + // given + given(providerComposite.findProvider(지원하는_소셜_로그인_타입)).willReturn(userInfoProvider); + given(userInfoProvider.findUserInformation(anyString())).willReturn(가입하지_않은_사용자_회원_정보); + + // when & then + assertThatThrownBy(() -> profileImageNotFoundAuthenticationService.login(지원하는_소셜_로그인_타입, 유효한_소셜_로그인_토큰, 디바이스_토큰)) + .isInstanceOf(ImageNotFoundException.class) + .hasMessage("기본 이미지를 찾을 수 없습니다."); + } - given(mockProviderComposite.findProvider(Oauth2Type.KAKAO)).willReturn(mockProvider); - given(mockProvider.findUserInformation(anyString())).willReturn(userInformationDto); + @Test + void 가입하지_않은_회원이_소셜_로그인을_할_경우_accessToken과_refreshToken을_반환한다() { + // given + given(providerComposite.findProvider(지원하는_소셜_로그인_타입)).willReturn(userInfoProvider); + given(userInfoProvider.findUserInformation(anyString())).willReturn(사용자_회원_정보); // when - final TokenDto actual = authenticationService.login(Oauth2Type.KAKAO, "accessToken"); + final TokenDto actual = authenticationService.login(지원하는_소셜_로그인_타입, 유효한_소셜_로그인_토큰, 디바이스_토큰); // then SoftAssertions.assertSoftly(softAssertions -> { @@ -127,15 +160,13 @@ void setUp( } @Test - void 가입하지_않은_회원이_소셜_로그인을_할_경우_accessToken과_refreshToken을_반환한다() { + void 탈퇴한_회원이_소셜_로그인을_할_경우_accessToken과_refreshToken을_반환한다() { // given - final UserInformationDto userInformationDto = new UserInformationDto(12345L); - - given(mockProviderComposite.findProvider(Oauth2Type.KAKAO)).willReturn(mockProvider); - given(mockProvider.findUserInformation(anyString())).willReturn(userInformationDto); + given(providerComposite.findProvider(지원하는_소셜_로그인_타입)).willReturn(userInfoProvider); + given(userInfoProvider.findUserInformation(anyString())).willReturn(사용자_회원_정보); // when - final TokenDto actual = authenticationService.login(Oauth2Type.KAKAO, "accessToken"); + final TokenDto actual = authenticationService.login(지원하는_소셜_로그인_타입, 유효한_소셜_로그인_토큰, 디바이스_토큰); // then SoftAssertions.assertSoftly(softAssertions -> { @@ -146,16 +177,8 @@ void setUp( @Test void refreshToken을_전달하면_새로운_accessToken을_반환한다() { - // given - final Map privateClaims = Map.of("userId", 1L); - final String refreshToken = "Bearer " + tokenEncoder.encode( - LocalDateTime.now(), - TokenType.REFRESH, - privateClaims - ); - // when - final TokenDto actual = authenticationService.refreshToken(refreshToken); + final TokenDto actual = authenticationService.refreshToken(유효한_리프레시_토큰); // then SoftAssertions.assertSoftly(softAssertions -> { @@ -166,51 +189,24 @@ void setUp( @Test void 만료된_refreshToken으로_새로운_accessToken을_요청하면_예외가_발생한다() { - // given - final Instant instant = Instant.parse("2023-01-01T22:21:20Z"); - final ZoneId zoneId = ZoneId.of("UTC"); - - given(clock.instant()).willReturn(instant); - given(clock.getZone()).willReturn(zoneId); - - final LocalDateTime targetTime = LocalDateTime.ofInstant(instant, zoneId); - - final Map privateClaims = Map.of("userId", "12345"); - final String refreshToken = "Bearer " + tokenEncoder.encode( - targetTime, - TokenType.REFRESH, - privateClaims - ); - // when & then - assertThatThrownBy(() -> authenticationService.refreshToken(refreshToken)) + assertThatThrownBy(() -> authenticationService.refreshToken(만료된_리프레시_토큰)) .isInstanceOf(InvalidTokenException.class) .hasMessage("유효한 토큰이 아닙니다."); } @Test void 유효한_토큰_타입이_아닌_refreshToken으로_새로운_accessToken을_요청하면_예외가_발생한다() { - // given - final String invalidRefreshToken = "invalidRefreshToken"; - // when & then - assertThatThrownBy(() -> authenticationService.refreshToken(invalidRefreshToken)) + assertThatThrownBy(() -> authenticationService.refreshToken(유효하지_않은_타입의_리프레시_토큰)) .isInstanceOf(InvalidTokenException.class) .hasMessage("Bearer 타입이 아닙니다."); } @Test void 유효한_accessToken을_검증하면_참을_반환한다() { - // given - final Map privateClaims = Map.of("userId", 1L); - final String accessToken = "Bearer " + tokenEncoder.encode( - LocalDateTime.now(), - TokenType.ACCESS, - privateClaims - ); - // when - final boolean actual = authenticationService.validateToken(accessToken); + final boolean actual = authenticationService.validateToken(유효한_액세스_토큰); // then assertThat(actual).isTrue(); @@ -218,21 +214,72 @@ void setUp( @Test void 만료된_accessToken을_검증하면_거짓을_반환한다() { + // when + final boolean actual = authenticationService.validateToken(만료된_소셜_로그인_토큰); + + // then + assertThat(actual).isFalse(); + } + + @Test + void 가입한_회원이_탈퇴하는_경우_정상처리한다() throws InvalidWithdrawalException { // given - final Instant instant = Instant.parse("2000-08-10T15:30:00Z"); - final LocalDateTime expiredPublishTime = instant.atZone(ZoneId.of("UTC")).toLocalDateTime(); - - final Map privateClaims = Map.of("userId", 1L); - final String accessToken = "Bearer " + tokenEncoder.encode( - expiredPublishTime, - TokenType.ACCESS, - privateClaims - ); + given(providerComposite.findProvider(지원하는_소셜_로그인_타입)).willReturn(userInfoProvider); + given(userInfoProvider.findUserInformation(anyString())).willReturn(사용자_회원_정보); // when - final boolean actual = authenticationService.validateToken(accessToken); + authenticationService.withdrawal(지원하는_소셜_로그인_타입, 유효한_액세스_토큰, 유효한_리프레시_토큰); // then - assertThat(actual).isFalse(); + assertThat(사용자.isDeleted()).isTrue(); + } + + @Test + void 이미_탈퇴한_회원이_탈퇴하는_경우_예외가_발생한다() { + given(providerComposite.findProvider(지원하는_소셜_로그인_타입)).willReturn(userInfoProvider); + given(userInfoProvider.findUserInformation(anyString())).willReturn(탈퇴한_사용자_회원_정보); + given(userInfoProvider.unlinkUserBy(anyString())).willReturn(탈퇴한_사용자_회원_정보); + + // when && then + assertThatThrownBy(() -> authenticationService.withdrawal(지원하는_소셜_로그인_타입, 탈퇴한_사용자_액세스_토큰, 유효한_리프레시_토큰)) + .isInstanceOf(InvalidWithdrawalException.class) + .hasMessage("탈퇴에 대한 권한 없습니다."); + } + + @Test + void 존재하지_않는_회원이_탈퇴하는_경우_예외가_발생한다() { + // given + given(providerComposite.findProvider(지원하는_소셜_로그인_타입)).willReturn(userInfoProvider); + given(userInfoProvider.findUserInformation(anyString())).willThrow(new InvalidTokenException("401 Unauthorized")); + + // when & then + assertThatThrownBy(() -> authenticationService.withdrawal(지원하는_소셜_로그인_타입, 존재하지_않는_사용자_액세스_토큰, 유효한_리프레시_토큰)) + .isInstanceOf(InvalidWithdrawalException.class) + .hasMessage("탈퇴에 대한 권한 없습니다."); + } + + @Test + void 탈퇴할_때_유효한_토큰이_아닌_경우_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> authenticationService.withdrawal(지원하는_소셜_로그인_타입, 유효하지_않은_액세스_토큰, 유효한_리프레시_토큰)) + .isInstanceOf(InvalidTokenException.class) + .hasMessage("유효한 토큰이 아닙니다."); + } + + @Test + void 로그인할_때_가입하지_않은_사용자라면_회원가입을_진행한다() { + // given + given(providerComposite.findProvider(지원하는_소셜_로그인_타입)).willReturn(userInfoProvider); + given(userInfoProvider.findUserInformation(anyString())).willReturn(가입하지_않은_사용자_회원_정보); + + // when + final TokenDto actual = authenticationService.login(지원하는_소셜_로그인_타입, 유효한_소셜_로그인_토큰, 디바이스_토큰); + + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual.accessToken()).isNotEmpty().contains("Bearer "); + softAssertions.assertThat(actual.refreshToken()).isNotEmpty().contains("Bearer "); + }); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/authentication/application/AuthenticationUserServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/authentication/application/AuthenticationUserServiceTest.java index e5a7108ed..65e026c46 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/authentication/application/AuthenticationUserServiceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/authentication/application/AuthenticationUserServiceTest.java @@ -2,9 +2,8 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.ddang.ddang.authentication.application.fixture.AuthenticationUserServiceFixture; import com.ddang.ddang.configuration.IsolateDatabase; -import com.ddang.ddang.user.domain.User; -import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -13,29 +12,15 @@ @IsolateDatabase @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class AuthenticationUserServiceTest { - - @Autowired - JpaUserRepository userRepository; +class AuthenticationUserServiceTest extends AuthenticationUserServiceFixture { @Autowired AuthenticationUserService authenticationUserService; @Test void 회원탈퇴한_회원의_id를_전달하면_참을_반환한다() { - // given - final User user = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - user.withdrawal(); - userRepository.save(user); - // when - final boolean actual = authenticationUserService.isWithdrawal(user.getId()); + final boolean actual = authenticationUserService.isWithdrawal(탈퇴한_사용자.getId()); // then assertThat(actual).isTrue(); @@ -43,18 +28,8 @@ class AuthenticationUserServiceTest { @Test void 회원탈퇴하지_않거나_회원가입하지_않은_회원의_id를_전달하면_거짓을_반환한다() { - // given - final User user = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(user); - // when - final boolean actual = authenticationUserService.isWithdrawal(user.getId()); + final boolean actual = authenticationUserService.isWithdrawal(사용자.getId()); // then assertThat(actual).isFalse(); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/authentication/application/BlackListTokenServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/authentication/application/BlackListTokenServiceTest.java index 4c3a6b7ad..abacc94fe 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/authentication/application/BlackListTokenServiceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/authentication/application/BlackListTokenServiceTest.java @@ -4,13 +4,10 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import com.ddang.ddang.authentication.domain.TokenEncoder; +import com.ddang.ddang.authentication.application.fixture.BlackListTokenServiceFixture; import com.ddang.ddang.authentication.domain.TokenType; import com.ddang.ddang.authentication.domain.exception.EmptyTokenException; -import com.ddang.ddang.authentication.infrastructure.persistence.JpaBlackListTokenRepository; import com.ddang.ddang.configuration.IsolateDatabase; -import java.time.LocalDateTime; -import java.util.Map; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -21,28 +18,15 @@ @IsolateDatabase @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class BlackListTokenServiceTest { +class BlackListTokenServiceTest extends BlackListTokenServiceFixture { @Autowired BlackListTokenService blackListTokenService; - @Autowired - JpaBlackListTokenRepository blackListTokenRepository; - - @Autowired - TokenEncoder tokenEncoder; - @Test void 유효한_accessToken과_refreshToken을_전달하면_블랙리스트로_등록한다() { - // given - final Map privateClaims = Map.of("userId", 1L); - final String accessToken = - "Bearer " + tokenEncoder.encode(LocalDateTime.now(), TokenType.ACCESS, privateClaims); - final String refreshToken = - "Bearer " + tokenEncoder.encode(LocalDateTime.now(), TokenType.REFRESH, privateClaims); - // when & then - assertDoesNotThrow(() -> blackListTokenService.registerBlackListToken(accessToken, refreshToken)); + assertDoesNotThrow(() -> blackListTokenService.registerBlackListToken(유효한_액세스_토큰, 유효한_리프레시_토큰)); } @ParameterizedTest @@ -59,17 +43,8 @@ class BlackListTokenServiceTest { @Test void 블랙리스트로_등록된_토큰인지_확인할때_이미_블랙리스트로_등록된_토큰을_전달하면_참을_반환한다() { - // given - final Map privateClaims = Map.of("userId", 1L); - final String accessToken = - "Bearer " + tokenEncoder.encode(LocalDateTime.now(), TokenType.ACCESS, privateClaims); - final String refreshToken = - "Bearer " + tokenEncoder.encode(LocalDateTime.now(), TokenType.REFRESH, privateClaims); - - blackListTokenService.registerBlackListToken(accessToken, refreshToken); - // when - final boolean actual = blackListTokenService.existsBlackListToken(TokenType.ACCESS, accessToken); + final boolean actual = blackListTokenService.existsBlackListToken(TokenType.ACCESS, 만료된_액세스_토큰); // then assertThat(actual).isTrue(); @@ -77,11 +52,8 @@ class BlackListTokenServiceTest { @Test void 블랙리스트로_등록된_토큰인지_확인할때_블랙리스트로_등록되지_않은_토큰을_전달하면_거짓을_반환한다() { - // given - final String invalidAccessToken = "invalidAccessToken"; - // when - final boolean actual = blackListTokenService.existsBlackListToken(TokenType.ACCESS, invalidAccessToken); + final boolean actual = blackListTokenService.existsBlackListToken(TokenType.ACCESS, 유효한_액세스_토큰); // then assertThat(actual).isFalse(); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/authentication/application/fixture/AuthenticationServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/authentication/application/fixture/AuthenticationServiceFixture.java new file mode 100644 index 000000000..19ef0f791 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/authentication/application/fixture/AuthenticationServiceFixture.java @@ -0,0 +1,128 @@ +package com.ddang.ddang.authentication.application.fixture; + +import com.ddang.ddang.authentication.domain.TokenEncoder; +import com.ddang.ddang.authentication.domain.TokenType; +import com.ddang.ddang.authentication.domain.dto.UserInformationDto; +import com.ddang.ddang.authentication.infrastructure.oauth2.Oauth2Type; +import com.ddang.ddang.device.domain.DeviceToken; +import com.ddang.ddang.device.infrastructure.persistence.JpaDeviceTokenRepository; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.image.infrastructure.persistence.JpaProfileImageRepository; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Map; + +@SuppressWarnings("NonAsciiCharacters") +public class AuthenticationServiceFixture { + + protected Oauth2Type 지원하는_소셜_로그인_타입 = Oauth2Type.KAKAO; + protected Oauth2Type 지원하지_않는_소셜_로그인_타입 = Oauth2Type.KAKAO; + + protected String 유효한_소셜_로그인_토큰 = "Bearer accessToken"; + protected String 만료된_소셜_로그인_토큰; + + protected String 디바이스_토큰 = "deviceToken"; + + protected User 사용자; + protected User 탈퇴한_사용자; + + protected UserInformationDto 사용자_회원_정보 = new UserInformationDto(12345L); + protected UserInformationDto 탈퇴한_사용자_회원_정보 = new UserInformationDto(54321L); + protected UserInformationDto 가입하지_않은_사용자_회원_정보 = new UserInformationDto(-99999L); + + protected String 유효한_액세스_토큰; + protected String 유효하지_않은_액세스_토큰 = "Bearer invalidAccessToken"; + protected String 탈퇴한_사용자_액세스_토큰; + protected String 이미지가_없는_사용자_액세스_토큰; + protected String 존재하지_않는_사용자_액세스_토큰; + protected String 유효한_리프레시_토큰; + protected String 만료된_리프레시_토큰; + protected String 유효하지_않은_타입의_리프레시_토큰 = "invalidRefreshToken"; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaProfileImageRepository profileImageRepository; + + @Autowired + private TokenEncoder tokenEncoder; + + @Autowired + private JpaDeviceTokenRepository deviceTokenRepository; + + @BeforeEach + void fixtureSetUp() { + profileImageRepository.save(new ProfileImage("default_profile_image.png", "default_profile_image.png")); + + 사용자 = User.builder() + .name("kakao12345") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(0.0d)) + .oauthId("12345") + .build(); + + 탈퇴한_사용자 = User.builder() + .name("kakao12346") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(0.0d)) + .oauthId("12346") + .build(); + + userRepository.save(사용자); + 탈퇴한_사용자.withdrawal(); + userRepository.save(탈퇴한_사용자); + + final DeviceToken deviceToken = new DeviceToken(사용자, 디바이스_토큰); + deviceTokenRepository.save(deviceToken); + + 유효한_리프레시_토큰 = tokenEncoder.encode( + LocalDateTime.now(), + TokenType.REFRESH, + Map.of("userId", 1L) + ); + + 만료된_리프레시_토큰 = tokenEncoder.encode( + LocalDateTime.ofInstant(Instant.parse("2023-01-01T22:21:20Z"), ZoneId.of("UTC")), + TokenType.REFRESH, + Map.of("userId", 1L) + ); + + 만료된_소셜_로그인_토큰 = tokenEncoder.encode( + Instant.parse("2000-08-10T15:30:00Z").atZone(ZoneId.of("UTC")).toLocalDateTime(), + TokenType.ACCESS, + Map.of("userId", 1L) + ); + + 유효한_액세스_토큰 = tokenEncoder.encode( + LocalDateTime.now(), + TokenType.ACCESS, + Map.of("userId", 1L) + ); + + 탈퇴한_사용자_액세스_토큰 = tokenEncoder.encode( + LocalDateTime.now(), + TokenType.ACCESS, + Map.of("userId", 탈퇴한_사용자.getId()) + ); + + 이미지가_없는_사용자_액세스_토큰 = tokenEncoder.encode( + LocalDateTime.now(), + TokenType.ACCESS, + Map.of("userId", 가입하지_않은_사용자_회원_정보.id()) + ); + + 존재하지_않는_사용자_액세스_토큰 = tokenEncoder.encode( + LocalDateTime.now(), + TokenType.ACCESS, + Map.of("userId", -99999L) + ); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/authentication/application/fixture/AuthenticationUserServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/authentication/application/fixture/AuthenticationUserServiceFixture.java new file mode 100644 index 000000000..b7281f200 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/authentication/application/fixture/AuthenticationUserServiceFixture.java @@ -0,0 +1,40 @@ +package com.ddang.ddang.authentication.application.fixture; + +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +@SuppressWarnings("NonAsciiCharacters") +public class AuthenticationUserServiceFixture { + + @Autowired + private JpaUserRepository userRepository; + + protected User 사용자; + protected User 탈퇴한_사용자; + + @BeforeEach + void setUp() { + 사용자 = User.builder() + .name("kakao12345") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(0.0d)) + .oauthId("12345") + .build(); + + 탈퇴한_사용자 = User.builder() + .name("kakao12346") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(0.0d)) + .oauthId("12346") + .build(); + + userRepository.save(사용자); + userRepository.save(탈퇴한_사용자); + + 탈퇴한_사용자.withdrawal(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/authentication/application/fixture/BlackListTokenServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/authentication/application/fixture/BlackListTokenServiceFixture.java new file mode 100644 index 000000000..1e9b09bbf --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/authentication/application/fixture/BlackListTokenServiceFixture.java @@ -0,0 +1,52 @@ +package com.ddang.ddang.authentication.application.fixture; + +import com.ddang.ddang.authentication.domain.BlackListToken; +import com.ddang.ddang.authentication.domain.TokenEncoder; +import com.ddang.ddang.authentication.domain.TokenType; +import com.ddang.ddang.authentication.infrastructure.persistence.JpaBlackListTokenRepository; +import java.time.LocalDateTime; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +@SuppressWarnings("NonAsciiCharacters") +public class BlackListTokenServiceFixture { + + @Autowired + private JpaBlackListTokenRepository blackListTokenRepository; + + @Autowired + private TokenEncoder tokenEncoder; + + protected String 유효한_액세스_토큰; + protected String 유효한_리프레시_토큰; + protected String 만료된_액세스_토큰; + protected String 만료된_리프레시_토큰; + + @BeforeEach + void setUp() { + 유효한_액세스_토큰 = tokenEncoder.encode( + LocalDateTime.now(), + TokenType.ACCESS, + Map.of("userId", 1L) + ); + 유효한_리프레시_토큰 = tokenEncoder.encode( + LocalDateTime.now(), + TokenType.REFRESH, + Map.of("userId", 1L) + ); + 만료된_액세스_토큰 = tokenEncoder.encode( + LocalDateTime.now(), + TokenType.ACCESS, + Map.of("userId", 2L) + ); + 만료된_리프레시_토큰 = tokenEncoder.encode( + LocalDateTime.now(), + TokenType.REFRESH, + Map.of("userId", 2L) + ); + + blackListTokenRepository.save(new BlackListToken(TokenType.ACCESS, 만료된_액세스_토큰)); + blackListTokenRepository.save(new BlackListToken(TokenType.REFRESH, 만료된_리프레시_토큰)); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/jwt/JwtDecoderTest.java b/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/jwt/JwtDecoderTest.java index 76a3d2c16..0d5639c07 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/jwt/JwtDecoderTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/jwt/JwtDecoderTest.java @@ -3,45 +3,26 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import com.ddang.ddang.authentication.configuration.JwtConfigurationProperties; import com.ddang.ddang.authentication.domain.TokenType; import com.ddang.ddang.authentication.domain.exception.InvalidTokenException; -import java.time.LocalDateTime; -import java.util.Map; +import com.ddang.ddang.authentication.infrastructure.jwt.fixture.JwtDecoderFixture; import java.util.Optional; -import org.junit.jupiter.api.BeforeEach; +import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class JwtDecoderTest { - - JwtDecoder jwtDecoder; - - JwtEncoder jwtEncoder; - - @BeforeEach - void setUp() { - final JwtConfigurationProperties jwtConfigurationProperties = new JwtConfigurationProperties( - "thisistoolargeaccesstokenkeyfordummykeydataforlocal", - "thisistoolargerefreshtokenkeyfordummykeydataforlocal", - 12L, - 1460L - ); - - jwtDecoder = new JwtDecoder(jwtConfigurationProperties); - jwtEncoder = new JwtEncoder(jwtConfigurationProperties); - } +class JwtDecoderTest extends JwtDecoderFixture { @Test void 토큰의_길이가_유효하지_않다면_예외가_발생한다() { // given - final String invalidLengthToken = "abcde"; + final JwtDecoder jwtDecoder = new JwtDecoder(토큰_설정); // when & then - assertThatThrownBy(() -> jwtDecoder.decode(TokenType.ACCESS, invalidLengthToken)) + assertThatThrownBy(() -> jwtDecoder.decode(TokenType.ACCESS, 유효하지_않은_길이의_토큰)) .isInstanceOf(InvalidTokenException.class) .hasMessage("Bearer 타입이 아니거나 유효한 토큰이 아닙니다."); } @@ -49,10 +30,10 @@ void setUp() { @Test void 토큰의_타입이_유효하지_않다면_예외가_발생한다() { // given - final String invalidTypeToken = "Basic12 abcde"; + final JwtDecoder jwtDecoder = new JwtDecoder(토큰_설정); // when & then - assertThatThrownBy(() -> jwtDecoder.decode(TokenType.ACCESS, invalidTypeToken)) + assertThatThrownBy(() -> jwtDecoder.decode(TokenType.ACCESS, 유효하지_않은_타입의_토큰)) .isInstanceOf(InvalidTokenException.class) .hasMessage("Bearer 타입이 아닙니다."); } @@ -60,10 +41,10 @@ void setUp() { @Test void 토큰이_유효하지_않다면_빈_Optional을_반환한다() { // given - final String invalidToken = "Bearer adf"; + final JwtDecoder jwtDecoder = new JwtDecoder(토큰_설정); // when - final Optional actual = jwtDecoder.decode(TokenType.ACCESS, invalidToken); + final Optional actual = jwtDecoder.decode(TokenType.ACCESS, 유효하지_않은_토큰); // then assertThat(actual).isEmpty(); @@ -72,13 +53,15 @@ void setUp() { @Test void 유효한_토큰이면_해당_토큰의_본문을_반환한다() { // given - final Map privateClaims = Map.of("userId", 1L); - final String accessToken = "Bearer " + jwtEncoder.encode(LocalDateTime.now(), TokenType.ACCESS, privateClaims); + final JwtDecoder jwtDecoder = new JwtDecoder(토큰_설정); // when - final Optional actual = jwtDecoder.decode(TokenType.ACCESS, accessToken); + final Optional actual = jwtDecoder.decode(TokenType.ACCESS, 유효한_토큰); // then - assertThat(actual).isPresent(); + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isPresent(); + softAssertions.assertThat(actual.get().userId()).isEqualTo(토큰_내용.get(회원_아이디_키)); + }); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/jwt/JwtEncoderTest.java b/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/jwt/JwtEncoderTest.java index 8afc1d7a2..df2cd5313 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/jwt/JwtEncoderTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/jwt/JwtEncoderTest.java @@ -2,41 +2,27 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.ddang.ddang.authentication.configuration.JwtConfigurationProperties; import com.ddang.ddang.authentication.domain.TokenType; +import com.ddang.ddang.authentication.infrastructure.jwt.fixture.JwtEncoderFixture; import java.time.LocalDateTime; -import java.util.Map; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class JwtEncoderTest { - - JwtEncoder jwtEncoder; - - @BeforeEach - void setUp() { - final JwtConfigurationProperties jwtConfigurationProperties = new JwtConfigurationProperties( - "thisistoolargeaccesstokenkeyfordummykeydataforlocal", - "thisistoolargerefreshtokenkeyfordummykeydataforlocal", - 12L, - 1460L - ); - jwtEncoder = new JwtEncoder(jwtConfigurationProperties); - } +class JwtEncoderTest extends JwtEncoderFixture { @Test void 토큰을_생성한다() { // given - final Map privateClaims = Map.of("userId", 1L); + final JwtEncoder jwtEncoder = new JwtEncoder(토큰_설정); // when - final String actual = jwtEncoder.encode(LocalDateTime.now(), TokenType.ACCESS, privateClaims); + final String actual = jwtEncoder.encode(LocalDateTime.now(), TokenType.ACCESS, 토큰_내용); // then - assertThat(actual).isNotBlank(); + assertThat(actual).isNotBlank() + .contains("Bearer "); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/jwt/fixture/JwtDecoderFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/jwt/fixture/JwtDecoderFixture.java new file mode 100644 index 000000000..05dd3bc42 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/jwt/fixture/JwtDecoderFixture.java @@ -0,0 +1,25 @@ +package com.ddang.ddang.authentication.infrastructure.jwt.fixture; + +import com.ddang.ddang.authentication.configuration.JwtConfigurationProperties; +import com.ddang.ddang.authentication.domain.TokenType; +import com.ddang.ddang.authentication.infrastructure.jwt.JwtEncoder; +import java.time.LocalDateTime; +import java.util.Map; + +@SuppressWarnings("NonAsciiCharacters") +public class JwtDecoderFixture { + + protected String 유효하지_않은_길이의_토큰 = "abcde"; + protected String 유효하지_않은_토큰 = "Bearer abcde"; + protected String 유효하지_않은_타입의_토큰 = "Basic12 abcde"; + protected JwtConfigurationProperties 토큰_설정 = new JwtConfigurationProperties( + "thisistoolargeaccesstokenkeyfordummykeydataforlocal", + "thisistoolargerefreshtokenkeyfordummykeydataforlocal", + 12L, + 1460L + ); + protected JwtEncoder 토큰_암호화_생성기 = new JwtEncoder(토큰_설정); + protected Map 토큰_내용 = Map.of("userId", 1L); + protected String 유효한_토큰 = 토큰_암호화_생성기.encode(LocalDateTime.now(), TokenType.ACCESS, 토큰_내용); + protected String 회원_아이디_키 = "userId"; +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/jwt/fixture/JwtEncoderFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/jwt/fixture/JwtEncoderFixture.java new file mode 100644 index 000000000..f1feaa878 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/jwt/fixture/JwtEncoderFixture.java @@ -0,0 +1,16 @@ +package com.ddang.ddang.authentication.infrastructure.jwt.fixture; + +import com.ddang.ddang.authentication.configuration.JwtConfigurationProperties; +import java.util.Map; + +@SuppressWarnings("NonAsciiCharacters") +public class JwtEncoderFixture { + + protected JwtConfigurationProperties 토큰_설정 = new JwtConfigurationProperties( + "thisistoolargeaccesstokenkeyfordummykeydataforlocal", + "thisistoolargerefreshtokenkeyfordummykeydataforlocal", + 12L, + 1460L + ); + protected final Map 토큰_내용 = Map.of("userId", 1L); +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/oauth2/KakaoOauth2TypeTest.java b/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/oauth2/KakaoOauth2TypeTest.java index c6a99dd0a..52b44ac39 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/oauth2/KakaoOauth2TypeTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/oauth2/KakaoOauth2TypeTest.java @@ -3,23 +3,20 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import com.ddang.ddang.authentication.domain.dto.UserInformationDto; import com.ddang.ddang.authentication.domain.exception.UnsupportedSocialLoginException; +import com.ddang.ddang.authentication.infrastructure.oauth2.fixture.KakaoOauth2TypeFixture; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class KakaoOauth2TypeTest { +class KakaoOauth2TypeTest extends KakaoOauth2TypeFixture { @Test void 지원하는_소셜_로그인을_전달하면_해당_소셜_로그인_타입을_반환한다() { - // given - final String typeName = "kakao"; - // when - final Oauth2Type actual = Oauth2Type.from(typeName); + final Oauth2Type actual = Oauth2Type.from(카카오_소셜_로그인_방식); // then assertThat(actual).isEqualTo(Oauth2Type.KAKAO); @@ -27,24 +24,19 @@ class KakaoOauth2TypeTest { @Test void 지원하지_않는_소셜_로그인을_전달하면_예외가_발생한다() { - // given - final String invalidTypeName = "naver"; - // when & then - assertThatThrownBy(() -> Oauth2Type.from(invalidTypeName)) + assertThatThrownBy(() -> Oauth2Type.from(지원하지_않는_소셜_로그인_방식)) .isInstanceOf(UnsupportedSocialLoginException.class) .hasMessage("지원하는 소셜 로그인 기능이 아닙니다."); } @Test void 카카오_회원_ID를_전달하면_고유한_닉네임을_반환한다() { - // given - final UserInformationDto userInformationDto = new UserInformationDto(12345L); - // when - final String actual = Oauth2Type.KAKAO.calculateNickname(userInformationDto); + final String actual = Oauth2Type.KAKAO.calculateNickname(카카오_회원_식별자); // then - assertThat(actual).isEqualTo("kakao12345"); + assertThat(actual).contains(카카오_소셜_로그인_방식) + .contains(카카오_회원_식별자); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/oauth2/fixture/KakaoOauth2TypeFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/oauth2/fixture/KakaoOauth2TypeFixture.java new file mode 100644 index 000000000..8125d0908 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/oauth2/fixture/KakaoOauth2TypeFixture.java @@ -0,0 +1,9 @@ +package com.ddang.ddang.authentication.infrastructure.oauth2.fixture; + +@SuppressWarnings("NonAsciiCharacters") +public class KakaoOauth2TypeFixture { + + protected String 카카오_소셜_로그인_방식 = "kakao"; + protected String 지원하지_않는_소셜_로그인_방식 = "navaer"; + protected String 카카오_회원_식별자 = "12345"; +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/oauth2/kakao/KakaoUserInformationProviderTest.java b/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/oauth2/kakao/KakaoUserInformationProviderTest.java index 2659612f8..23bf1027c 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/oauth2/kakao/KakaoUserInformationProviderTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/oauth2/kakao/KakaoUserInformationProviderTest.java @@ -11,6 +11,7 @@ import com.ddang.ddang.authentication.configuration.Oauth2PropertiesConfiguration; import com.ddang.ddang.authentication.domain.dto.UserInformationDto; import com.ddang.ddang.authentication.infrastructure.oauth2.Oauth2Type; +import com.ddang.ddang.authentication.infrastructure.oauth2.kakao.fixture.KakaoUserInformationProviderFixture; import com.ddang.ddang.configuration.RestTemplateConfiguration; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; @@ -28,25 +29,24 @@ @Import({RestTemplateConfiguration.class, Oauth2PropertiesConfiguration.class}) @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class KakaoUserInformationProviderTest { - - MockRestServiceServer mockRestServiceServer; +class KakaoUserInformationProviderTest extends KakaoUserInformationProviderFixture { @Autowired RestTemplate restTemplate; - @Autowired - KakaoUserInformationProvider provider; - @Autowired KakaoProvidersConfigurationProperties kakaoProperties; @Autowired - ObjectMapper objectMapper; + KakaoUserInformationProvider provider; + + MockRestServiceServer kakaoServer; + + ObjectMapper objectMapper = new ObjectMapper(); @BeforeEach void setUp() { - mockRestServiceServer = MockRestServiceServer.createServer(restTemplate); + kakaoServer = MockRestServiceServer.createServer(restTemplate); } @Test @@ -59,36 +59,48 @@ void setUp() { @Test void 유효한_카카오_토큰을_전달한_경우_회원_정보를_조회한다() throws Exception { // given - final UserInformationDto userInformationDto = new UserInformationDto(12345L); - - mockRestServiceServer.expect(requestTo(matchesPattern(kakaoProperties.userInfoUri()))) - .andRespond( - withSuccess( - objectMapper.writeValueAsString(userInformationDto), - MediaType.APPLICATION_JSON - ) - ); - - final String accessToken = "Bearer accessToken"; + kakaoServer.expect(requestTo(matchesPattern(kakaoProperties.userInfoUri()))) + .andRespond( + withSuccess( + objectMapper.writeValueAsString(회원_정보), + MediaType.APPLICATION_JSON + ) + ); // when - final UserInformationDto actual = provider.findUserInformation(accessToken); + final UserInformationDto actual = provider.findUserInformation(유효한_토큰); // then - assertThat(actual.id()).isEqualTo(userInformationDto.id()); + assertThat(actual).isEqualTo(회원_정보); } @Test void 유효하지_않은_카카오_토큰을_전달한_경우_예외가_발생한다() { // given - final String invalidAccessToken = "Bearer accessToken"; - - mockRestServiceServer.expect(requestTo(matchesPattern(kakaoProperties.userInfoUri()))) - .andRespond(withUnauthorizedRequest()); + kakaoServer.expect(requestTo(matchesPattern(kakaoProperties.userInfoUri()))) + .andRespond(withUnauthorizedRequest()); // when & then - assertThatThrownBy(() -> provider.findUserInformation(invalidAccessToken)) + assertThatThrownBy(() -> provider.findUserInformation(유효하지_않은_토큰)) .isInstanceOf(RuntimeException.class) .hasMessage("401 Unauthorized"); } + + @Test + void 유효한_카카오_토큰을_전달한_경우_카카오_연결을_끊는다() throws Exception { + // given + kakaoServer.expect(requestTo(matchesPattern(kakaoProperties.userUnlinkUri()))) + .andRespond( + withSuccess( + objectMapper.writeValueAsString(회원_정보), + MediaType.APPLICATION_JSON + ) + ); + + // when + final UserInformationDto actual = provider.unlinkUserBy(카카오_회원_식별자); + + // then + assertThat(actual).isEqualTo(회원_정보); + } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/oauth2/kakao/fixture/KakaoUserInformationProviderFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/oauth2/kakao/fixture/KakaoUserInformationProviderFixture.java new file mode 100644 index 000000000..d6aeaaf88 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/oauth2/kakao/fixture/KakaoUserInformationProviderFixture.java @@ -0,0 +1,14 @@ +package com.ddang.ddang.authentication.infrastructure.oauth2.kakao.fixture; + +import com.ddang.ddang.authentication.domain.dto.UserInformationDto; +import org.springframework.test.web.client.MockRestServiceServer; + +@SuppressWarnings("NonAsciiCharacters") +public class KakaoUserInformationProviderFixture { + + protected MockRestServiceServer 카카오_인증_서버; + protected String 카카오_회원_식별자 = "12345"; + protected UserInformationDto 회원_정보 = new UserInformationDto(12345L); + protected String 유효한_토큰 = "Bearer accessToken"; + protected String 유효하지_않은_토큰 = "Bearer invalidAccessToken"; +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/persistence/JpaBlackListTokenRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/persistence/JpaBlackListTokenRepositoryTest.java index 935740396..8633ce4a5 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/persistence/JpaBlackListTokenRepositoryTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/persistence/JpaBlackListTokenRepositoryTest.java @@ -4,10 +4,9 @@ import com.ddang.ddang.authentication.domain.BlackListToken; import com.ddang.ddang.authentication.domain.TokenType; +import com.ddang.ddang.authentication.infrastructure.persistence.fixture.JpaBlackListTokenRepositoryFixture; import com.ddang.ddang.configuration.JpaConfiguration; import com.ddang.ddang.configuration.QuerydslConfiguration; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -16,24 +15,18 @@ import org.springframework.context.annotation.Import; @DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -@Import({JpaConfiguration.class, QuerydslConfiguration.class}) -class JpaBlackListTokenRepositoryTest { - - @PersistenceContext - EntityManager em; +class JpaBlackListTokenRepositoryTest extends JpaBlackListTokenRepositoryFixture { @Autowired JpaBlackListTokenRepository blackListTokenRepository; @Test void BlackListToken_엔티티를_저장한다() { - // given - final BlackListToken blackListToken = new BlackListToken(TokenType.ACCESS, "accessToken"); - // when - final BlackListToken actual = blackListTokenRepository.save(blackListToken); + final BlackListToken actual = blackListTokenRepository.save(만료할_토큰); // then assertThat(actual.getId()).isPositive(); @@ -41,17 +34,9 @@ class JpaBlackListTokenRepositoryTest { @Test void 블랙리스트로_등록된_토큰인지_확인할때_이미_블랙리스트로_등록된_토큰을_전달하면_참을_반환한다() { - // given - final BlackListToken blackListToken = new BlackListToken(TokenType.ACCESS, "accessToken"); - - blackListTokenRepository.save(blackListToken); - - em.flush(); - em.clear(); - // when final boolean actual = blackListTokenRepository - .existsByTokenTypeAndToken(TokenType.ACCESS, "accessToken"); + .existsByTokenTypeAndToken(TokenType.ACCESS, 만료_토큰_내용); // then assertThat(actual).isTrue(); @@ -59,12 +44,9 @@ class JpaBlackListTokenRepositoryTest { @Test void 블랙리스트로_등록된_토큰인지_확인할때_블랙리스트로_등록되지_않은_토큰을_전달하면_거짓을_반환한다() { - // given - final String invalidAccessToken = "invalidAccessToken"; - // when final boolean actual = blackListTokenRepository - .existsByTokenTypeAndToken(TokenType.ACCESS, invalidAccessToken); + .existsByTokenTypeAndToken(TokenType.ACCESS, 만료되지_않은_토큰_내용); // then assertThat(actual).isFalse(); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/persistence/fixture/JpaBlackListTokenRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/persistence/fixture/JpaBlackListTokenRepositoryFixture.java new file mode 100644 index 000000000..3ea72710a --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/authentication/infrastructure/persistence/fixture/JpaBlackListTokenRepositoryFixture.java @@ -0,0 +1,32 @@ +package com.ddang.ddang.authentication.infrastructure.persistence.fixture; + +import com.ddang.ddang.authentication.domain.BlackListToken; +import com.ddang.ddang.authentication.domain.TokenType; +import com.ddang.ddang.authentication.infrastructure.persistence.JpaBlackListTokenRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +@SuppressWarnings("NonAsciiCharacters") +public class JpaBlackListTokenRepositoryFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaBlackListTokenRepository blackListTokenRepository; + + protected String 만료_토큰_내용 = "expired token"; + protected String 만료되지_않은_토큰_내용 = "not expired token"; + protected BlackListToken 만료할_토큰 = new BlackListToken(TokenType.ACCESS, 만료되지_않은_토큰_내용); + protected BlackListToken 만료된_토큰 = new BlackListToken(TokenType.ACCESS, 만료_토큰_내용); + + @BeforeEach + void setUp() { + blackListTokenRepository.save(만료된_토큰); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/authentication/presentation/AuthenticationControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/authentication/presentation/AuthenticationControllerTest.java index 4b4f23c7e..13a707c9f 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/authentication/presentation/AuthenticationControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/authentication/presentation/AuthenticationControllerTest.java @@ -1,5 +1,6 @@ package com.ddang.ddang.authentication.presentation; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; @@ -18,73 +19,31 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.ddang.ddang.authentication.application.AuthenticationService; -import com.ddang.ddang.authentication.application.BlackListTokenService; -import com.ddang.ddang.authentication.application.dto.TokenDto; +import com.ddang.ddang.authentication.application.exception.InvalidWithdrawalException; import com.ddang.ddang.authentication.configuration.Oauth2TypeConverter; import com.ddang.ddang.authentication.domain.exception.InvalidTokenException; import com.ddang.ddang.authentication.domain.exception.UnsupportedSocialLoginException; -import com.ddang.ddang.authentication.infrastructure.oauth2.Oauth2Type; -import com.ddang.ddang.authentication.presentation.dto.request.AccessTokenRequest; -import com.ddang.ddang.authentication.presentation.dto.request.LogoutRequest; -import com.ddang.ddang.authentication.presentation.dto.request.RefreshTokenRequest; -import com.ddang.ddang.configuration.RestDocsConfiguration; +import com.ddang.ddang.authentication.presentation.fixture.AuthenticationControllerFixture; import com.ddang.ddang.exception.GlobalExceptionHandler; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; -import org.springframework.context.annotation.Import; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@WebMvcTest(controllers = {AuthenticationController.class}, - excludeFilters = { - @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfigurer.class), - @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com\\.ddang\\.ddang\\.authentication\\.configuration\\..*") - } -) -@AutoConfigureRestDocs -@Import(RestDocsConfiguration.class) -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -@SuppressWarnings("NonAsciiCharacters") -class AuthenticationControllerTest { - - @MockBean - AuthenticationService authenticationService; - - @MockBean - BlackListTokenService blackListTokenService; - - @Autowired - AuthenticationController authenticationController; - @Autowired - RestDocumentationResultHandler restDocs; - - @Autowired - ObjectMapper objectMapper; +@SuppressWarnings("NonAsciiCharacters") +class AuthenticationControllerTest extends AuthenticationControllerFixture { MockMvc mockMvc; @BeforeEach - void setUp(@Autowired RestDocumentationContextProvider provider) { + void setUp() { final FormattingConversionService formattingConversionService = new FormattingConversionService(); formattingConversionService.addConverter(new Oauth2TypeConverter()); @@ -100,49 +59,33 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { @Test void 소셜_로그인을_지원하는_타입과_소셜_로그인_토큰을_전달하면_accessToken과_refreshToken을_반환한다() throws Exception { // given - final TokenDto tokenDto = new TokenDto("accessToken", "refreshToken"); - final AccessTokenRequest request = new AccessTokenRequest("kakaoAccessToken"); - - given(authenticationService.login(eq(Oauth2Type.KAKAO), anyString())).willReturn(tokenDto); + given(authenticationService.login(eq(지원하는_소셜_로그인_타입), anyString(), anyString())).willReturn(발급된_토큰); // when & then - mockMvc.perform(RestDocumentationRequestBuilders.post("/oauth2/login/{oauth2Type}", "kakao") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) - ) - .andExpectAll( - status().isOk(), - jsonPath("$.accessToken").exists(), - jsonPath("$.refreshToken").exists() - ) - .andDo( - restDocs.document( - pathParameters( - parameterWithName("oauth2Type").description("소셜 로그인을 할 서비스 선택(kakao로 고정)") - ), - requestFields( - fieldWithPath("accessToken").description("소셜 로그인 AccessToken") - ), - responseFields( - fieldWithPath("accessToken").type(JsonFieldType.STRING).description("Access Token"), - fieldWithPath("refreshToken").type(JsonFieldType.STRING).description("Refresh Token") - ) + final ResultActions resultActions = + mockMvc.perform(RestDocumentationRequestBuilders.post("/oauth2/login/{oauth2Type}", 소셜_로그인_타입) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(유효한_로그인_요청)) ) - ); + .andExpectAll( + status().isOk(), + jsonPath("$.accessToken").exists(), + jsonPath("$.refreshToken").exists() + ); + + login_문서화(resultActions); } @Test void 소셜_로그인을_진행하지_않는_타입을_전달하면_400이_발생한다() throws Exception { // given - final AccessTokenRequest request = new AccessTokenRequest("kakaoAccessToken"); - - given(authenticationService.login(eq(Oauth2Type.KAKAO), anyString())) + given(authenticationService.login(eq(지원하지_않는_소셜_로그인_타입), anyString(), anyString())) .willThrow(new UnsupportedSocialLoginException("지원하는 소셜 로그인 기능이 아닙니다.")); // when & then - mockMvc.perform(post("/oauth2/login/{oauth2Type}", "kakao") + mockMvc.perform(post("/oauth2/login/{oauth2Type}", 소셜_로그인_타입) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) + .content(objectMapper.writeValueAsString(유효하지_않은_로그인_요청)) ) .andExpectAll( status().isBadRequest(), @@ -153,16 +96,13 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { @Test void 유효하지_않은_소셜_로그인_토큰을_전달하면_401이_발생한다() throws Exception { // given - final String invalidKakaoAccessToken = "invalidKakaoAccessToken"; - final AccessTokenRequest request = new AccessTokenRequest(invalidKakaoAccessToken); - - given(authenticationService.login(eq(Oauth2Type.KAKAO), anyString())) + given(authenticationService.login(eq(지원하는_소셜_로그인_타입), anyString(), anyString())) .willThrow(new InvalidTokenException("401 Unauthorized", new RuntimeException())); // when & then - mockMvc.perform(post("/oauth2/login/{oauth2Type}", "kakao") + mockMvc.perform(post("/oauth2/login/{oauth2Type}", 소셜_로그인_타입) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) + .content(objectMapper.writeValueAsString(유효하지_않은_로그인_요청)) ) .andExpectAll( status().isUnauthorized(), @@ -173,48 +113,32 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { @Test void 유효한_refreshToken을_전달하면_새로운_accessToken과_refreshToken을_반환한다() throws Exception { // given - final TokenDto tokenDto = new TokenDto("accessToken", "refreshToken"); - final RefreshTokenRequest request = new RefreshTokenRequest("refreshToken"); - - given(authenticationService.refreshToken(anyString())).willReturn(tokenDto); + given(authenticationService.refreshToken(anyString())).willReturn(발급된_토큰); // when & then - mockMvc.perform(post("/oauth2/refresh-token") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) - ) - .andExpectAll( - status().isOk(), - jsonPath("$.accessToken").exists(), - jsonPath("$.refreshToken").exists() - ) - .andDo( - restDocs.document( - requestFields( - fieldWithPath("refreshToken").description("refreshToken") - ), - responseFields( - fieldWithPath("accessToken").type(JsonFieldType.STRING).description("재발급한 Access Token"), - fieldWithPath("refreshToken").type(JsonFieldType.STRING).description("기존 Refresh Token") - ) - ) - ); - ; + final ResultActions resultActions = mockMvc.perform(post("/oauth2/refresh-token") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(유효한_토큰_재발급_요청)) + ) + .andExpectAll( + status().isOk(), + jsonPath("$.accessToken").exists(), + jsonPath("$.refreshToken").exists() + ); + + refreshToken_문서화(resultActions); } @Test void 유효하지_않은_refreshToken을_전달하면_401을_반환한다() throws Exception { // given - final String invalidRefreshToken = "invalidRefreshToken"; - final RefreshTokenRequest request = new RefreshTokenRequest(invalidRefreshToken); - willThrow(new InvalidTokenException("유효한 토큰이 아닙니다.")).given(authenticationService) .refreshToken(anyString()); // when & then mockMvc.perform(post("/oauth2/refresh-token") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) + .content(objectMapper.writeValueAsString(유효하지_않은_토큰_재발급_요청)) ) .andExpectAll( status().isUnauthorized(), @@ -228,24 +152,16 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { given(authenticationService.validateToken(anyString())).willReturn(true); // when & then - mockMvc.perform(get("/oauth2/validate-token") - .contentType(MediaType.APPLICATION_JSON) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - ) - .andExpectAll( - status().isOk(), - jsonPath("$.validated").value(true) - ) - .andDo( - restDocs.document( - requestHeaders( - headerWithName("Authorization").description("회원 Bearer 인증 정보") - ), - responseFields( - fieldWithPath("validated").type(JsonFieldType.BOOLEAN).description("Access Token이 유효한지 여부") - ) - ) - ); + final ResultActions resultActions = mockMvc.perform(get("/oauth2/validate-token") + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, 유효한_액세스_토큰_내용) + ) + .andExpectAll( + status().isOk(), + jsonPath("$.validated").value(true) + ); + + validateToken_문서화(resultActions); } @Test @@ -256,7 +172,7 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { // when & then mockMvc.perform(get("/oauth2/validate-token") .contentType(MediaType.APPLICATION_JSON) - .header(HttpHeaders.AUTHORIZATION, "Bearer invalidAccessToken") + .header(HttpHeaders.AUTHORIZATION, 만료된_액세스_토큰_내용) ) .andExpectAll( status().isOk(), @@ -267,28 +183,134 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { @Test void accessToken과_refreshToken을_전달하면_로그아웃한다() throws Exception { // given - final LogoutRequest request = new LogoutRequest("Bearer refreshToken"); - willDoNothing().given(blackListTokenService).registerBlackListToken(anyString(), anyString()); // when & then - mockMvc.perform(post("/oauth2/logout") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + final ResultActions resultActions = mockMvc.perform(post("/oauth2/logout") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(유효한_로그아웃_요청)) + .header(HttpHeaders.AUTHORIZATION, 유효한_액세스_토큰_내용) + ) + .andExpectAll( + status().isNoContent() + ); + + logout_문서화(resultActions); + } + + @Test + void ouath2Type과_accessToken과_refreshToken을_전달하면_탈퇴한다() throws Exception { + // given + willDoNothing().given(authenticationService).withdrawal(any(), anyString(), anyString()); + + // when & then + final ResultActions resultActions = + mockMvc.perform(RestDocumentationRequestBuilders.post("/oauth2/withdrawal/{oauth2Type}", 소셜_로그인_타입) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(유효한_회원탈퇴_요청)) + .header(HttpHeaders.AUTHORIZATION, 유효한_액세스_토큰_내용) + ) + .andExpectAll( + status().isNoContent() + ); + + withdrawal_문서화(resultActions); + } + + @Test + void ouath2Type과_accessToken과_refreshToken을_전달시_이미_탈퇴_혹은_존재하지_않아_권한이_없는_회원인_경우_403을_반환한다() throws Exception { + // given + willThrow(new InvalidWithdrawalException("탈퇴에 대한 권한 없습니다.")).given(authenticationService) + .withdrawal(any(), anyString(), anyString()); + + // when & then + mockMvc.perform(RestDocumentationRequestBuilders.post("/oauth2/withdrawal/{oauth2Type}", 소셜_로그인_타입) + .header(HttpHeaders.AUTHORIZATION, 유효한_액세스_토큰_내용) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(유효하지_않은_회원탈퇴_요청)) ) .andExpectAll( - status().isNoContent() - ) - .andDo( - restDocs.document( - requestHeaders( - headerWithName("Authorization").description("회원 Bearer 인증 정보") - ), - requestFields( - fieldWithPath("refreshToken").description("refreshToken") - ) - ) + status().isForbidden(), + jsonPath("$.message").value("탈퇴에 대한 권한 없습니다.") ); } + + private void login_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + pathParameters( + parameterWithName("oauth2Type").description("소셜 로그인을 할 서비스 선택(kakao로 고정)") + ), + requestFields( + fieldWithPath("accessToken").description("소셜 로그인 AccessToken"), + fieldWithPath("deviceToken").description("기기 디바이스 토큰") + ), + responseFields( + fieldWithPath("accessToken").type(JsonFieldType.STRING) + .description("Access Token"), + fieldWithPath("refreshToken").type(JsonFieldType.STRING) + .description("Refresh Token") + ) + ) + ); + } + + private void refreshToken_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestFields( + fieldWithPath("refreshToken").description("refreshToken") + ), + responseFields( + fieldWithPath("accessToken").type(JsonFieldType.STRING) + .description("재발급한 Access Token"), + fieldWithPath("refreshToken").type(JsonFieldType.STRING) + .description("기존 Refresh Token") + ) + ) + ); + } + + private void validateToken_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + responseFields( + fieldWithPath("validated").type(JsonFieldType.BOOLEAN) + .description("Access Token이 유효한지 여부") + ) + ) + ); + } + + private void logout_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + requestFields( + fieldWithPath("refreshToken").description("refreshToken") + ) + ) + ); + } + + private void withdrawal_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + pathParameters( + parameterWithName("oauth2Type").description("소셜 로그인을 할 서비스 선택(kakao로 고정)") + ), + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + requestFields( + fieldWithPath("refreshToken").description("refreshToken") + ) + ) + ); + } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/authentication/presentation/fixture/AuthenticationControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/authentication/presentation/fixture/AuthenticationControllerFixture.java new file mode 100644 index 000000000..ee3145fee --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/authentication/presentation/fixture/AuthenticationControllerFixture.java @@ -0,0 +1,27 @@ +package com.ddang.ddang.authentication.presentation.fixture; + +import com.ddang.ddang.authentication.application.dto.TokenDto; +import com.ddang.ddang.authentication.infrastructure.oauth2.Oauth2Type; +import com.ddang.ddang.authentication.presentation.dto.request.LoginTokenRequest; +import com.ddang.ddang.authentication.presentation.dto.request.LogoutRequest; +import com.ddang.ddang.authentication.presentation.dto.request.RefreshTokenRequest; +import com.ddang.ddang.authentication.presentation.dto.request.WithdrawalRequest; +import com.ddang.ddang.configuration.CommonControllerSliceTest; + +@SuppressWarnings("NonAsciiCharacters") +public class AuthenticationControllerFixture extends CommonControllerSliceTest { + + protected Oauth2Type 지원하는_소셜_로그인_타입 = Oauth2Type.KAKAO; + protected Oauth2Type 지원하지_않는_소셜_로그인_타입 = Oauth2Type.KAKAO; + protected String 소셜_로그인_타입 = "kakao"; + protected String 유효한_액세스_토큰_내용 = "Bearer accessToken"; + protected String 만료된_액세스_토큰_내용 = "Bearer accessToken"; + protected TokenDto 발급된_토큰 = new TokenDto(유효한_액세스_토큰_내용, "Bearer refreshToken"); + protected LoginTokenRequest 유효한_로그인_요청 = new LoginTokenRequest("kakaoAccessToken", "deviceToken"); + protected LoginTokenRequest 유효하지_않은_로그인_요청 = new LoginTokenRequest("kakaoAccessToken", "deviceToken"); + protected RefreshTokenRequest 유효한_토큰_재발급_요청 = new RefreshTokenRequest("Bearer refreshToken"); + protected RefreshTokenRequest 유효하지_않은_토큰_재발급_요청 = new RefreshTokenRequest("Basic refreshToken"); + protected LogoutRequest 유효한_로그아웃_요청 = new LogoutRequest("Bearer refreshToken"); + protected WithdrawalRequest 유효한_회원탈퇴_요청 = new WithdrawalRequest("Bearer refreshToken"); + protected WithdrawalRequest 유효하지_않은_회원탈퇴_요청 = new WithdrawalRequest("Bearer refreshToken"); +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/bid/application/BidServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/bid/application/BidServiceTest.java index 1a57ec098..1d6572636 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/bid/application/BidServiceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/bid/application/BidServiceTest.java @@ -1,167 +1,88 @@ package com.ddang.ddang.bid.application; import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; -import com.ddang.ddang.auction.domain.Auction; -import com.ddang.ddang.auction.domain.BidUnit; -import com.ddang.ddang.auction.domain.Price; -import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; import com.ddang.ddang.bid.application.dto.CreateBidDto; import com.ddang.ddang.bid.application.dto.ReadBidDto; +import com.ddang.ddang.bid.application.event.BidNotificationEvent; import com.ddang.ddang.bid.application.exception.InvalidAuctionToBidException; import com.ddang.ddang.bid.application.exception.InvalidBidPriceException; import com.ddang.ddang.bid.application.exception.InvalidBidderException; +import com.ddang.ddang.bid.application.fixture.BidServiceFixture; import com.ddang.ddang.configuration.IsolateDatabase; +import com.ddang.ddang.notification.application.NotificationService; +import com.ddang.ddang.notification.application.dto.CreateNotificationDto; import com.ddang.ddang.user.application.exception.UserNotFoundException; -import com.ddang.ddang.user.domain.User; -import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import com.google.firebase.messaging.FirebaseMessagingException; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.event.ApplicationEvents; +import org.springframework.test.context.event.RecordApplicationEvents; -import java.time.LocalDateTime; import java.util.List; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; @IsolateDatabase +@RecordApplicationEvents @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class BidServiceTest { +class BidServiceTest extends BidServiceFixture { @Autowired BidService bidService; - @Autowired - JpaAuctionRepository auctionRepository; + @MockBean + NotificationService notificationService; @Autowired - JpaUserRepository userRepository; + ApplicationEvents events; @Test void 입찰을_등록한다() { - // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User user = User.builder() - .name("사용자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - - userRepository.save(seller); - auctionRepository.save(auction); - userRepository.save(user); - - final CreateBidDto createBidDto = new CreateBidDto(auction.getId(), 10_000, user.getId()); - // when - final Long actual = bidService.create(createBidDto); + final Long actual = bidService.create(입찰_요청_dto, 이미지_절대_url); // then SoftAssertions.assertSoftly(softAssertions -> { softAssertions.assertThat(actual).isPositive(); - softAssertions.assertThat(auction.getLastBid().getPrice().getValue()).isEqualTo(createBidDto.bidPrice()); - softAssertions.assertThat(auction.getAuctioneerCount()).isEqualTo(1); + softAssertions.assertThat(입찰_내역이_없는_경매.getLastBid().getPrice().getValue()).isEqualTo(입찰_요청_dto.bidPrice()); + softAssertions.assertThat(입찰_내역이_없는_경매.getAuctioneerCount()).isEqualTo(1); }); } @Test - void 마지막_입찰자와_다른_사람은_마지막_입찰액과_최소_입찰단위를_더한_금액_이상의_금액으로_입찰을_등록할_수_있다() { + void 마지막_입찰자와_다른_사람은_마지막_입찰액과_최소_입찰단위를_더한_금액_이상의_금액으로_입찰을_등록할_수_있다() throws FirebaseMessagingException { // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User user1 = User.builder() - .name("사용자1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - final User user2 = User.builder() - .name("사용자2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12347") - .build(); - - userRepository.save(seller); - auctionRepository.save(auction); - userRepository.save(user1); - userRepository.save(user2); - - final CreateBidDto createBidDto1 = new CreateBidDto(auction.getId(), 10_000, user1.getId()); - final CreateBidDto createBidDto2 = new CreateBidDto(auction.getId(), 14_000, user2.getId()); - - bidService.create(createBidDto1); + given(notificationService.send(any(CreateNotificationDto.class))).willReturn(알림_성공); // when - final Long actual = bidService.create(createBidDto2); + final Long actual = bidService.create(입찰_내역이_하나_존재하는_경매에_대한_입찰_요청_dto, 이미지_절대_url); + final long eventActual = events.stream(BidNotificationEvent.class).count(); // then - assertThat(actual).isPositive(); + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isPositive(); + softAssertions.assertThat(입찰_내역이_하나_있던_경매.getLastBid().getPrice().getValue()) + .isEqualTo(입찰_내역이_하나_존재하는_경매에_대한_입찰_요청_dto.bidPrice()); + softAssertions.assertThat(입찰_내역이_하나_있던_경매.getAuctioneerCount()).isEqualTo(2); + softAssertions.assertThat(eventActual).isEqualTo(1); + }); } @Test void 첫_입찰자는_시작가를_입찰로_등록할_수_있다() { - // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User buyer = User.builder() - .name("사용자2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - - userRepository.save(seller); - auctionRepository.save(auction); - userRepository.save(buyer); - - final CreateBidDto createBidDto = new CreateBidDto(auction.getId(), 1_000, buyer.getId()); - // when - final Long actual = bidService.create(createBidDto); + final Long actual = bidService.create(첫입찰자가_시작가로_입찰_요청_dto, 이미지_절대_url); // then assertThat(actual).isPositive(); @@ -169,414 +90,106 @@ class BidServiceTest { @Test void 존재하지_않는_경매에_입찰하는_경우_예외가_발생한다() { - // given - final Long invaliAuctionId = -9999L; - - final User user = User.builder() - .name("사용자1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - userRepository.save(user); - - final CreateBidDto createBidDto = new CreateBidDto(invaliAuctionId, 10_000, user.getId()); - // when & then - assertThatThrownBy(() -> bidService.create(createBidDto)) + assertThatThrownBy(() -> bidService.create(존재하지_않는_경매_아이디에_대한_입찰_요청_dto, 이미지_절대_url)) .isInstanceOf(AuctionNotFoundException.class) .hasMessage("해당 경매를 찾을 수 없습니다."); } @Test void 존재하지_않는_사용자가_경매에_입찰하는_경우_예외가_발생한다() { - // given - final Long invalidUserId = -9999L; - - final Auction auction = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - - auctionRepository.save(auction); - - final CreateBidDto createBidDto = new CreateBidDto(auction.getId(), 10_000, invalidUserId); - // when & then - assertThatThrownBy(() -> bidService.create(createBidDto)) + assertThatThrownBy(() -> bidService.create(존재하지_않는_사용자_아이디를_통한_입찰_요청_dto, 이미지_절대_url)) .isInstanceOf(UserNotFoundException.class) .hasMessage("해당 사용자를 찾을 수 없습니다."); } @Test void 종료된_경매에_입찰하는_경우_예외가_발생한다() { - // given - final Auction auction = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().minusDays(1)) - .build(); - final User user = User.builder() - .name("사용자1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - auctionRepository.save(auction); - userRepository.save(user); - - final CreateBidDto createBidDto = new CreateBidDto(auction.getId(), 10_000, user.getId()); - // when & then - assertThatThrownBy(() -> bidService.create(createBidDto)) + assertThatThrownBy(() -> bidService.create(종료된_경매에_대한_입찰_요청_dto, 이미지_절대_url)) .isInstanceOf(InvalidAuctionToBidException.class) .hasMessage("이미 종료된 경매입니다"); } @Test void 삭제된_경매에_입찰하는_경우_예외가_발생한다() { - // given - final Auction auction = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User user = User.builder() - .name("사용자1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - auctionRepository.save(auction); - userRepository.save(user); - - final CreateBidDto createBidDto = new CreateBidDto(auction.getId(), 10_000, user.getId()); - auction.delete(); - // when & then - assertThatThrownBy(() -> bidService.create(createBidDto)) + assertThatThrownBy(() -> bidService.create(삭제된_경매에_대한_입찰_요청_dto, 이미지_절대_url)) .isInstanceOf(InvalidAuctionToBidException.class) .hasMessage("삭제된 경매입니다"); } @Test void 판매자가_입찰하는_경우_예외가_발생한다() { - final User user = User.builder() - .name("사용자1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .seller(user) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - - userRepository.save(user); - auctionRepository.save(auction); - - final CreateBidDto createBidDto = new CreateBidDto(auction.getId(), 10_000, user.getId()); - // when && then - assertThatThrownBy(() -> bidService.create(createBidDto)) + assertThatThrownBy(() -> bidService.create(판매자가_본인_경매에_입찰_요청_dto, 이미지_절대_url)) .isInstanceOf(InvalidBidderException.class) .hasMessage("판매자는 입찰할 수 없습니다"); } @Test - void 첫_입찰자가_시작가_낮은_금액으로_입찰하는_경우_예외가_발생한다() { - // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User user = User.builder() - .name("사용자1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - - userRepository.save(seller); - auctionRepository.save(auction); - userRepository.save(user); - - final CreateBidDto createBidDto = new CreateBidDto(auction.getId(), 900, user.getId()); - + void 첫_입찰자가_시작가보다_낮은_금액으로_입찰하는_경우_예외가_발생한다() { // when && then - assertThatThrownBy(() -> bidService.create(createBidDto)) + assertThatThrownBy(() -> bidService.create(첫입찰시_시작가보다_낮은_입찰액으로_입찰_요청_dto, 이미지_절대_url)) .isInstanceOf(InvalidBidPriceException.class) .hasMessage("입찰 금액이 잘못되었습니다"); } @Test void 마지막_입찰자가_연속으로_입찰하는_경우_예외가_발생한다() { - // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User user = User.builder() - .name("사용자1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - - userRepository.save(seller); - auctionRepository.save(auction); - userRepository.save(user); - - final CreateBidDto createBidDto1 = new CreateBidDto(auction.getId(), 10_000, user.getId()); - bidService.create(createBidDto1); - - final CreateBidDto createBidDto2 = new CreateBidDto(auction.getId(), 12_000, user.getId()); - // when && then - assertThatThrownBy(() -> bidService.create(createBidDto2)) + assertThatThrownBy(() -> bidService.create(동일한_사용자가_입찰_요청_dto, 이미지_절대_url)) .isInstanceOf(InvalidBidderException.class) .hasMessage("이미 최고 입찰자입니다"); } @Test void 마지막_입찰액보다_낮은_금액으로_입찰하는_경우_예외가_발생한다() { - // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User user1 = User.builder() - .name("사용자1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - final User user2 = User.builder() - .name("사용자2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12347") - .build(); - - userRepository.save(seller); - auctionRepository.save(auction); - userRepository.save(user1); - userRepository.save(user2); - - final CreateBidDto createBidDto1 = new CreateBidDto(auction.getId(), 10_000, user1.getId()); - bidService.create(createBidDto1); - - final CreateBidDto createBidDto2 = new CreateBidDto(auction.getId(), 8_000, user2.getId()); - // when & then - assertThatThrownBy(() -> bidService.create(createBidDto2)) + assertThatThrownBy(() -> bidService.create(이전_입찰액보다_낮은_입찰액으로_입찰_요청_dto, 이미지_절대_url)) .isInstanceOf(InvalidBidPriceException.class) .hasMessage("가능 입찰액보다 낮은 금액을 입력했습니다"); } @Test void 최소_입찰_단위보다_낮은_금액으로_입찰하는_경우_예외가_발생한다() { - // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User user1 = User.builder() - .name("사용자1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - final User user2 = User.builder() - .name("사용자2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12347") - .build(); - - userRepository.save(seller); - auctionRepository.save(auction); - userRepository.save(user1); - userRepository.save(user2); - - final CreateBidDto createBidDto1 = new CreateBidDto(auction.getId(), 10_000, user1.getId()); - bidService.create(createBidDto1); - - final CreateBidDto createBidDto2 = new CreateBidDto(auction.getId(), 10_500, user2.getId()); - // when & then - assertThatThrownBy(() -> bidService.create(createBidDto2)) + assertThatThrownBy(() -> bidService.create(최소_입찰단위를_더한_금액보다_낮은_입찰액으로_입찰_요청_dto, 이미지_절대_url)) .isInstanceOf(InvalidBidPriceException.class) .hasMessage("가능 입찰액보다 낮은 금액을 입력했습니다"); } @ParameterizedTest - @ValueSource(ints = {-1, 2_100_000_001}) - void 범위_밖의_금액으로_입찰하는_경우_예외가_발생한다(final int bidPrice) { - // given - final Auction auction = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User user = User.builder() - .name("사용자1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - auctionRepository.save(auction); - userRepository.save(user); - - final CreateBidDto createBidDto = new CreateBidDto(auction.getId(), bidPrice, user.getId()); - + @MethodSource("provideBidRequestWithOutOfBoundBidPrice") + void 범위_밖의_금액으로_입찰하는_경우_예외가_발생한다(final CreateBidDto 범위_밖의_금액으로_입찰_요청_dto) { // when & then - assertThatThrownBy(() -> bidService.create(createBidDto)) + assertThatThrownBy(() -> bidService.create(범위_밖의_금액으로_입찰_요청_dto, 이미지_절대_url)) .isInstanceOf(InvalidBidPriceException.class) .hasMessage("입찰 금액이 잘못되었습니다"); } + private static Stream provideBidRequestWithOutOfBoundBidPrice() { + return Stream.of(범위_밖의_금액으로_입찰_요청_dto1, 범위_밖의_금액으로_입찰_요청_dto2); + } + @Test void 특정_경매에_대한_입찰_목록을_조회한다() { - // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction1 = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final Auction auction2 = Auction.builder() - .seller(seller) - .title("경매 상품 2") - .description("이것은 경매 상품 2 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User user1 = User.builder() - .name("사용자1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - final User user2 = User.builder() - .name("사용자2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12347") - .build(); - - userRepository.save(seller); - auctionRepository.save(auction1); - auctionRepository.save(auction2); - userRepository.save(user1); - userRepository.save(user2); - - final CreateBidDto createBidDto1 = new CreateBidDto(auction1.getId(), 1_000, user1.getId()); - bidService.create(createBidDto1); - - final CreateBidDto createBidDto2 = new CreateBidDto(auction2.getId(), 1_000, user1.getId()); - bidService.create(createBidDto2); - - final CreateBidDto createBidDto3 = new CreateBidDto(auction1.getId(), 10_000, user2.getId()); - bidService.create(createBidDto3); - // when - final List actual = bidService.readAllByAuctionId(auction1.getId()); + final List actual = bidService.readAllByAuctionId(경매1.getId()); // then SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual.get(0).name()).isEqualTo(user1.getName()); - softAssertions.assertThat(actual.get(1).name()).isEqualTo(user2.getName()); + softAssertions.assertThat(actual).hasSize(2); + softAssertions.assertThat(actual.get(0).name()).isEqualTo(입찰자1.getName()); + softAssertions.assertThat(actual.get(1).name()).isEqualTo(입찰자2.getName()); }); } @Test void 특정_경매에_대한_입찰_내역이_없다면_빈배열을_반환한다() { - // given - final Auction auction1 = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User user1 = User.builder() - .name("사용자1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - auctionRepository.save(auction1); - userRepository.save(user1); - // when - final List actual = bidService.readAllByAuctionId(auction1.getId()); + final List actual = bidService.readAllByAuctionId(입찰_내역이_없는_경매.getId()); // then assertThat(actual).isEmpty(); @@ -584,11 +197,8 @@ class BidServiceTest { @Test void 입찰을_조회하려는_경매가_존재하지_않는_경우_예외를_반환한다() { - // given - final Long invalidAuctionId = -999L; - // when & then - assertThatThrownBy(() -> bidService.readAllByAuctionId(invalidAuctionId)) + assertThatThrownBy(() -> bidService.readAllByAuctionId(존재하지_않는_경매_아이디)) .isInstanceOf(AuctionNotFoundException.class) .hasMessage("해당 경매를 찾을 수 없습니다."); } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/bid/application/fixture/BidServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/bid/application/fixture/BidServiceFixture.java new file mode 100644 index 000000000..022acf6e8 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/bid/application/fixture/BidServiceFixture.java @@ -0,0 +1,161 @@ +package com.ddang.ddang.bid.application.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.bid.application.dto.CreateBidDto; +import com.ddang.ddang.bid.domain.Bid; +import com.ddang.ddang.bid.domain.BidPrice; +import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.notification.domain.NotificationStatus; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class BidServiceFixture { + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaBidRepository bidRepository; + + protected NotificationStatus 알림_성공 = NotificationStatus.SUCCESS; + protected String 이미지_절대_url = "https://3-ddang.store/auctions/images"; + protected Long 존재하지_않는_경매_아이디 = -999L; + protected Long 존재하지_않는_사용자_아이디 = -9999L; + + protected User 입찰자1; + protected User 입찰자2; + protected Auction 경매1; + protected Auction 입찰_내역이_없는_경매; + protected Auction 입찰_내역이_하나_있던_경매; + + protected CreateBidDto 입찰_요청_dto; + protected CreateBidDto 입찰_내역이_하나_존재하는_경매에_대한_입찰_요청_dto; + protected CreateBidDto 첫입찰자가_시작가로_입찰_요청_dto; + protected CreateBidDto 존재하지_않는_경매_아이디에_대한_입찰_요청_dto; + protected CreateBidDto 존재하지_않는_사용자_아이디를_통한_입찰_요청_dto; + protected CreateBidDto 종료된_경매에_대한_입찰_요청_dto; + protected CreateBidDto 삭제된_경매에_대한_입찰_요청_dto; + protected CreateBidDto 판매자가_본인_경매에_입찰_요청_dto; + protected CreateBidDto 첫입찰시_시작가보다_낮은_입찰액으로_입찰_요청_dto; + protected CreateBidDto 동일한_사용자가_입찰_요청_dto; + protected CreateBidDto 이전_입찰액보다_낮은_입찰액으로_입찰_요청_dto; + protected CreateBidDto 최소_입찰단위를_더한_금액보다_낮은_입찰액으로_입찰_요청_dto; + protected static CreateBidDto 범위_밖의_금액으로_입찰_요청_dto1; + protected static CreateBidDto 범위_밖의_금액으로_입찰_요청_dto2; + + @BeforeEach + void setUp() { + final ProfileImage 프로필_이미지 = new ProfileImage("upload.png", "store.png"); + final User 판매자 = User.builder() + .name("판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + 입찰자1 = User.builder() + .name("입찰자1") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + 입찰자2 = User.builder() + .name("입찰자2") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("78910") + .build(); + final AuctionImage 경매_이미지1 = new AuctionImage("auction_image.png", "auction_image.png"); + 경매1 = Auction.builder() + .seller(판매자) + .title("경매 상품 1") + .description("이것은 경매 상품 1 입니다.") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now().plusDays(7)) + .build(); + final AuctionImage 경매_이미지2 = new AuctionImage("auction_image.png", "auction_image.png"); + final Auction 경매2 = Auction.builder() + .seller(판매자) + .title("경매 상품 2") + .description("이것은 경매 상품 2 입니다.") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now().plusDays(7)) + .build(); + final AuctionImage 경매_이미지3 = new AuctionImage("auction_image.png", "auction_image.png"); + final Auction 경매3 = Auction.builder() + .seller(판매자) + .title("경매 상품 2") + .description("이것은 경매 상품 2 입니다.") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now().plusDays(7)) + .build(); + 입찰_내역이_없는_경매 = 경매3; + 입찰_내역이_하나_있던_경매 = 경매2; + final Auction 종료된_경매 = Auction.builder() + .seller(판매자) + .title("경매 상품 2") + .description("이것은 경매 상품 2 입니다.") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now().minusDays(7)) + .build(); + final Auction 삭제된_경매 = Auction.builder() + .seller(판매자) + .title("경매 상품 2") + .description("이것은 경매 상품 2 입니다.") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now().plusDays(7)) + .build(); + 삭제된_경매.delete(); + + userRepository.saveAll(List.of(판매자, 입찰자1, 입찰자2)); + 경매1.addAuctionImages(List.of(경매_이미지1)); + 경매2.addAuctionImages(List.of(경매_이미지2)); + 경매3.addAuctionImages(List.of(경매_이미지3)); + auctionRepository.saveAll(List.of(경매1, 경매2, 경매3, 입찰_내역이_하나_있던_경매, 종료된_경매, 삭제된_경매)); + + final Bid bid1 = new Bid(경매1, 입찰자1, new BidPrice(1_000)); + final Bid bid2 = new Bid(경매2, 입찰자1, new BidPrice(1_000)); + final Bid bid3 = new Bid(경매1, 입찰자2, new BidPrice(10_000)); + bidRepository.save(bid1); + bidRepository.save(bid2); + bidRepository.save(bid3); + + 경매1.updateLastBid(bid1); + 경매2.updateLastBid(bid2); + 경매1.updateLastBid(bid3); + + 입찰_요청_dto = new CreateBidDto(경매3.getId(), 10_000, 입찰자1.getId()); + 입찰_내역이_하나_존재하는_경매에_대한_입찰_요청_dto = new CreateBidDto(입찰_내역이_하나_있던_경매.getId(), 14_000, 입찰자2.getId()); + 첫입찰자가_시작가로_입찰_요청_dto = new CreateBidDto(경매3.getId(), 1_000, 입찰자1.getId()); + 존재하지_않는_경매_아이디에_대한_입찰_요청_dto = new CreateBidDto(존재하지_않는_경매_아이디, 10_000, 입찰자1.getId()); + 존재하지_않는_사용자_아이디를_통한_입찰_요청_dto = new CreateBidDto(경매3.getId(), 10_000, 존재하지_않는_사용자_아이디); + 종료된_경매에_대한_입찰_요청_dto = new CreateBidDto(종료된_경매.getId(), 10_000, 입찰자1.getId()); + 삭제된_경매에_대한_입찰_요청_dto = new CreateBidDto(삭제된_경매.getId(), 10_000, 입찰자1.getId()); + 판매자가_본인_경매에_입찰_요청_dto = new CreateBidDto(경매3.getId(), 10_000, 판매자.getId()); + 첫입찰시_시작가보다_낮은_입찰액으로_입찰_요청_dto = new CreateBidDto(경매3.getId(), 900, 입찰자1.getId()); + 동일한_사용자가_입찰_요청_dto = new CreateBidDto(입찰_내역이_하나_있던_경매.getId(), 12_000, 입찰자1.getId()); + 이전_입찰액보다_낮은_입찰액으로_입찰_요청_dto = new CreateBidDto(입찰_내역이_하나_있던_경매.getId(), 500, 입찰자2.getId()); + 최소_입찰단위를_더한_금액보다_낮은_입찰액으로_입찰_요청_dto = new CreateBidDto(입찰_내역이_하나_있던_경매.getId(), 1_500, 입찰자2.getId()); + 범위_밖의_금액으로_입찰_요청_dto1 = new CreateBidDto(경매3.getId(), -1, 입찰자2.getId()); + 범위_밖의_금액으로_입찰_요청_dto2 = new CreateBidDto(경매3.getId(), 2_100_000_001, 입찰자2.getId()); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/bid/domain/BidTest.java b/backend/ddang/src/test/java/com/ddang/ddang/bid/domain/BidTest.java index 9a61791c8..f52591c55 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/bid/domain/BidTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/bid/domain/BidTest.java @@ -1,43 +1,23 @@ package com.ddang.ddang.bid.domain; -import static org.assertj.core.api.Assertions.assertThat; - -import com.ddang.ddang.auction.domain.Auction; -import com.ddang.ddang.auction.domain.BidUnit; -import com.ddang.ddang.auction.domain.Price; -import com.ddang.ddang.user.domain.User; - -import java.time.LocalDateTime; - +import com.ddang.ddang.bid.domain.fixture.BidFixture; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class BidTest { +class BidTest extends BidFixture { @Test void 입찰자가_마지막_입찰자와_동일한_경우_참을_반환한다() { // given - final Auction auction = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User user = User.builder() - .name("사용자1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - final Bid bid = new Bid(auction, user, new BidPrice(10_000)); + final Bid bid = new Bid(경매, 입찰자, 입찰액); // when - final boolean actual = bid.isSameBidder(user); + final boolean actual = bid.isSameBidder(입찰자); // then assertThat(actual).isTrue(); @@ -46,25 +26,11 @@ class BidTest { @Test void 마지막_입찰액보다_낮은_금액으로_입찰하는_경우_참을_반환한다() { // given - final Auction auction = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User user = User.builder() - .name("사용자1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - final Bid bid = new Bid(auction, user, new BidPrice(10_000)); - auction.updateLastBid(bid); + final Bid bid = new Bid(경매, 입찰자, 입찰액); + 경매.updateLastBid(bid); // when - final boolean actual = bid.isNextBidPriceGreaterThan(new BidPrice(9_000)); + final boolean actual = bid.isNextBidPriceGreaterThan(이전_입찰액보다_작은_입찰액); // then assertThat(actual).isTrue(); @@ -73,25 +39,11 @@ class BidTest { @Test void 마지막_입찰액과_최소_입찰단위를_더한_금액보다_낮은_금액으로_입찰하는_경우_참을_반환한다() { // given - final Auction auction = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User user = User.builder() - .name("사용자1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - final Bid bid = new Bid(auction, user, new BidPrice(10_000)); - auction.updateLastBid(bid); + final Bid bid = new Bid(경매, 입찰자, 입찰액); + 경매.updateLastBid(bid); // when - final boolean actual = bid.isNextBidPriceGreaterThan(new BidPrice(10_900)); + final boolean actual = bid.isNextBidPriceGreaterThan(이전_입찰액보다_크지만_입찰_단위보다_작은_입찰액); // then assertThat(actual).isTrue(); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/bid/domain/fixture/BidFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/bid/domain/fixture/BidFixture.java new file mode 100644 index 000000000..154991818 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/bid/domain/fixture/BidFixture.java @@ -0,0 +1,58 @@ +package com.ddang.ddang.bid.domain.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.bid.domain.BidPrice; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import org.junit.jupiter.api.BeforeEach; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class BidFixture { + + protected User 판매자; + protected Auction 경매; + protected User 입찰자; + protected BidPrice 입찰액; + protected BidPrice 이전_입찰액보다_작은_입찰액; + protected BidPrice 이전_입찰액보다_크지만_입찰_단위보다_작은_입찰액; + + @BeforeEach + void setUp() { + final ProfileImage 프로필_이미지 = new ProfileImage("프로필.jpg", "프로필.jpg"); + 판매자 = User.builder() + .name("판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + + final AuctionImage 경매_이미지 = new AuctionImage("경매이미지.jpg", "경매이미지.jpg"); + 경매 = Auction.builder() + .seller(판매자) + .title("경매 상품") + .description("이것은 경매 상품입니다.") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 경매.addAuctionImages(List.of(경매_이미지)); + + 입찰자 = User.builder() + .name("입찰자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + + 입찰액 = new BidPrice(10_000); + 이전_입찰액보다_작은_입찰액 = new BidPrice(9_000); + 이전_입찰액보다_크지만_입찰_단위보다_작은_입찰액 = new BidPrice(10_900); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/bid/infrastructure/persistence/JpaBidRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/bid/infrastructure/persistence/JpaBidRepositoryTest.java index e466a475b..ab298d42b 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/bid/infrastructure/persistence/JpaBidRepositoryTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/bid/infrastructure/persistence/JpaBidRepositoryTest.java @@ -1,23 +1,11 @@ package com.ddang.ddang.bid.infrastructure.persistence; -import static org.assertj.core.api.Assertions.assertThat; - -import com.ddang.ddang.auction.domain.Auction; -import com.ddang.ddang.auction.domain.BidUnit; -import com.ddang.ddang.auction.domain.Price; -import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; import com.ddang.ddang.bid.domain.Bid; -import com.ddang.ddang.bid.domain.BidPrice; +import com.ddang.ddang.bid.infrastructure.persistence.fixture.JpaBidRepositoryFixture; import com.ddang.ddang.configuration.JpaConfiguration; import com.ddang.ddang.configuration.QuerydslConfiguration; -import com.ddang.ddang.user.domain.User; -import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; - -import java.time.LocalDateTime; -import java.util.List; - import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; @@ -26,81 +14,41 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + @DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -@Import({JpaConfiguration.class, QuerydslConfiguration.class}) -class JpaBidRepositoryTest { +class JpaBidRepositoryTest extends JpaBidRepositoryFixture { @PersistenceContext EntityManager em; - @Autowired - JpaAuctionRepository auctionRepository; - - @Autowired - JpaUserRepository userRepository; - @Autowired JpaBidRepository bidRepository; @Test void 입찰을_저장한다() { // given - final Auction auction = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final User user = User.builder() - .name("사용자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Bid bid = new Bid(auction, user, new BidPrice(10_000)); + final Bid bid = new Bid(경매1, 입찰자1, 입찰액); // when - auctionRepository.save(auction); - userRepository.save(user); - bidRepository.save(bid); + final Bid actual = bidRepository.save(bid); // then em.flush(); em.clear(); - assertThat(bid.getId()).isPositive(); + assertThat(actual.getId()).isPositive(); } @Test - void 특정_경매가_존재하는지_확인한다() { - // given - final Auction auction = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final User user = User.builder() - .name("사용자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Bid bid = new Bid(auction, user, new BidPrice(10_000)); - - auctionRepository.save(auction); - userRepository.save(user); - bidRepository.save(bid); - - em.flush(); - em.clear(); - + void 특정_경매에_입찰이_존재하는지_확인한다() { // when - final boolean actual = bidRepository.existsById(bid.getId()); + final boolean actual = bidRepository.existsById(경매1의_입찰1.getId()); // then assertThat(actual).isTrue(); @@ -108,92 +56,23 @@ class JpaBidRepositoryTest { @Test void 특정_경매의_입찰을_모두_조회한다() { - // given - final Auction auction1 = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final Auction auction2 = Auction.builder() - .title("경매 상품 2") - .description("이것은 경매 상품 2 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final User user = User.builder() - .name("사용자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Bid bid1 = new Bid(auction1, user, new BidPrice(10_000)); - final Bid bid2 = new Bid(auction1, user, new BidPrice(10_000)); - final Bid bid3 = new Bid(auction2, user, new BidPrice(10_000)); - - auctionRepository.save(auction1); - auctionRepository.save(auction2); - userRepository.save(user); - bidRepository.save(bid1); - bidRepository.save(bid2); - bidRepository.save(bid3); - - em.flush(); - em.clear(); - // when - final List actual = bidRepository.findByAuctionId(auction1.getId()); + final List actual = bidRepository.findByAuctionIdOrderByIdAsc(경매1.getId()); // then SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual.get(0).getId()).isEqualTo(bid1.getId()); - softAssertions.assertThat(actual.get(1).getId()).isEqualTo(bid2.getId()); + softAssertions.assertThat(actual).hasSize(2); + softAssertions.assertThat(actual.get(0).getId()).isEqualTo(경매1의_입찰1.getId()); + softAssertions.assertThat(actual.get(1).getId()).isEqualTo(경매1의_입찰2겸_마지막_입찰.getId()); }); } @Test void 특정_경매의_마지막_입찰을_조회한다() { - // given - final Auction auction1 = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final Auction auction2 = Auction.builder() - .title("경매 상품 2") - .description("이것은 경매 상품 2 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final User user = User.builder() - .name("사용자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Bid bid1 = new Bid(auction1, user, new BidPrice(10_000)); - final Bid bid2 = new Bid(auction1, user, new BidPrice(12_000)); - final Bid bid3 = new Bid(auction2, user, new BidPrice(10_000)); - - auctionRepository.save(auction1); - auctionRepository.save(auction2); - userRepository.save(user); - bidRepository.save(bid1); - bidRepository.save(bid2); - bidRepository.save(bid3); - - em.flush(); - em.clear(); - // when - final Bid actual = bidRepository.findLastBidByAuctionId(auction1.getId()); + final Bid actual = bidRepository.findLastBidByAuctionId(경매1.getId()); // then - assertThat(actual.getId()).isEqualTo(bid2.getId()); + assertThat(actual.getId()).isEqualTo(경매1의_입찰2겸_마지막_입찰.getId()); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/bid/infrastructure/persistence/fixture/JpaBidRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/bid/infrastructure/persistence/fixture/JpaBidRepositoryFixture.java new file mode 100644 index 000000000..ce7411126 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/bid/infrastructure/persistence/fixture/JpaBidRepositoryFixture.java @@ -0,0 +1,115 @@ +package com.ddang.ddang.bid.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.bid.domain.Bid; +import com.ddang.ddang.bid.domain.BidPrice; +import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class JpaBidRepositoryFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaCategoryRepository categoryRepository; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaBidRepository bidRepository; + + protected User 판매자; + protected Auction 경매1; + protected Auction 경매2; + protected User 입찰자1; + protected BidPrice 입찰액; + protected Bid 경매1의_입찰1; + protected Bid 경매1의_입찰2겸_마지막_입찰; + + @BeforeEach + void setUp() { + final ProfileImage 프로필_이미지 = new ProfileImage("프로필.jpg", "프로필.jpg"); + 판매자 = User.builder() + .name("판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + + final Category 전자기기_카테고리 = new Category("전자기기"); + final Category 전자기기_서브_노트북_카테고리 = new Category("노트북 카테고리"); + 전자기기_카테고리.addSubCategory(전자기기_서브_노트북_카테고리); + final AuctionImage 경매_이미지 = new AuctionImage("경매이미지.jpg", "경매이미지.jpg"); + 경매1 = Auction.builder() + .seller(판매자) + .title("경매 상품") + .description("이것은 경매 상품입니다.") + .subCategory(전자기기_서브_노트북_카테고리) + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 경매1.addAuctionImages(List.of(경매_이미지)); + 경매2 = Auction.builder() + .seller(판매자) + .title("경매 상품") + .description("이것은 경매 상품입니다.") + .subCategory(전자기기_서브_노트북_카테고리) + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 경매2.addAuctionImages(List.of(경매_이미지)); + + 입찰자1 = User.builder() + .name("입찰자1") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + final User 입찰자2 = User.builder() + .name("입찰자2") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + + 입찰액 = new BidPrice(10_000); + + 경매1의_입찰1 = new Bid(경매1, 입찰자1, 입찰액); + 경매1의_입찰2겸_마지막_입찰 = new Bid(경매1, 입찰자2, 입찰액); + final Bid 경매2의_입찰1 = new Bid(경매2, 입찰자1, 입찰액); + + userRepository.saveAll(List.of(판매자, 입찰자1, 입찰자2)); + + categoryRepository.saveAll(List.of(전자기기_카테고리, 전자기기_서브_노트북_카테고리)); + auctionRepository.saveAll(List.of(경매1, 경매2)); + + bidRepository.saveAll(List.of(경매1의_입찰1, 경매1의_입찰2겸_마지막_입찰, 경매2의_입찰1)); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/bid/presentation/BidControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/bid/presentation/BidControllerTest.java index 9c4d06d9e..13b617c15 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/bid/presentation/BidControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/bid/presentation/BidControllerTest.java @@ -1,113 +1,70 @@ package com.ddang.ddang.bid.presentation; -import static org.hamcrest.Matchers.is; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; -import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; -import com.ddang.ddang.authentication.application.AuthenticationUserService; -import com.ddang.ddang.authentication.application.BlackListTokenService; import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; import com.ddang.ddang.authentication.domain.TokenDecoder; import com.ddang.ddang.authentication.domain.TokenType; import com.ddang.ddang.authentication.domain.dto.AuthenticationStore; -import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims; -import com.ddang.ddang.bid.application.BidService; import com.ddang.ddang.bid.application.dto.CreateBidDto; -import com.ddang.ddang.bid.application.dto.ReadBidDto; import com.ddang.ddang.bid.application.exception.InvalidAuctionToBidException; import com.ddang.ddang.bid.application.exception.InvalidBidPriceException; import com.ddang.ddang.bid.application.exception.InvalidBidderException; import com.ddang.ddang.bid.presentation.dto.request.CreateBidRequest; -import com.ddang.ddang.configuration.RestDocsConfiguration; +import com.ddang.ddang.bid.presentation.fixture.BidControllerFixture; import com.ddang.ddang.exception.GlobalExceptionHandler; import com.ddang.ddang.user.application.exception.UserNotFoundException; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; -import org.springframework.context.annotation.Import; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@WebMvcTest(controllers = {BidController.class}, - excludeFilters = { - @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfigurer.class), - @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com\\.ddang\\.ddang\\.authentication\\.configuration\\..*") - } -) -@AutoConfigureRestDocs -@Import(RestDocsConfiguration.class) -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -@SuppressWarnings("NonAsciiCharacters") -class BidControllerTest { - - @MockBean - BidService bidService; - - @MockBean - BlackListTokenService blackListTokenService; - @MockBean - AuthenticationUserService authenticationUserService; - - @Autowired - BidController bidController; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; - @Autowired - RestDocumentationResultHandler restDocs; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @Autowired - ObjectMapper objectMapper; +@SuppressWarnings("NonAsciiCharacters") +class BidControllerTest extends BidControllerFixture { - TokenDecoder mockTokenDecoder; + TokenDecoder tokenDecoder; MockMvc mockMvc; @BeforeEach - void setUp(@Autowired RestDocumentationContextProvider provider) { - mockTokenDecoder = mock(TokenDecoder.class); + void setUp() { + tokenDecoder = mock(TokenDecoder.class); final AuthenticationStore store = new AuthenticationStore(); final AuthenticationInterceptor interceptor = new AuthenticationInterceptor( blackListTokenService, authenticationUserService, - mockTokenDecoder, + tokenDecoder, store ); final AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(store); @@ -125,399 +82,360 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { @Test void 입찰을_등록한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateBidRequest bidRequest = new CreateBidRequest(1L, 10_000); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(bidService.create(any(CreateBidDto.class))).willReturn(1L); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(bidService.create(any(CreateBidDto.class), anyString())).willReturn(생성된_입찰_아이디); // when & then - mockMvc.perform(post("/bids") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(bidRequest)) - ) - .andExpectAll( - status().isCreated(), - header().string(HttpHeaders.LOCATION, is("/auctions/1")) - ) - .andDo( - restDocs.document( - requestHeaders( - headerWithName("Authorization").description("회원 Bearer 인증 정보") - ), - requestFields( - fieldWithPath("auctionId").description("입찰할 경매 ID"), - fieldWithPath("bidPrice").description("입찰 금액") - ) - ) - ); + final ResultActions resultActions = mockMvc.perform(post("/bids") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(입찰_request)) + ) + .andExpectAll( + status().isCreated(), + header().string(HttpHeaders.LOCATION, is("/auctions/1")) + ); + + create_문서화(resultActions); } // TODO: 2023-08-06 예외 케이스 api 문서화의 경우 예외에 대한 변경이 없을 때 추가할 것 @Test void 해당_경매가_없는_경우_입찰시_404를_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final Long invalidAuctionId = 9999L; - final CreateBidRequest bidRequest = new CreateBidRequest(invalidAuctionId, 10_000); - final AuctionNotFoundException auctionNotFoundException = new AuctionNotFoundException("해당 경매를 찾을 수 없습니다."); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(bidService.create(any(CreateBidDto.class))).willThrow(auctionNotFoundException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(bidService.create(any(CreateBidDto.class), anyString())) + .willThrow(new AuctionNotFoundException("해당 경매를 찾을 수 없습니다.")); // when & then mockMvc.perform(post("/bids") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(bidRequest)) + .content(objectMapper.writeValueAsString(존재하지_않는_경매에_대한_입찰_request)) ) .andExpectAll( status().isNotFound(), - jsonPath("$.message", is(auctionNotFoundException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 해당_사용자가_없는_경우_입찰시_404를_반환한다() throws Exception { // given - final Long invalidUserId = 9999L; - final PrivateClaims privateClaims = new PrivateClaims(invalidUserId); - final CreateBidRequest bidRequest = new CreateBidRequest(1L, 10_000); - final UserNotFoundException userNotFoundException = new UserNotFoundException("해당 사용자를 찾을 수 없습니다."); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(bidService.create(any(CreateBidDto.class))) - .willThrow(userNotFoundException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(존재하지_않는_사용자_ID_클레임)); + given(bidService.create(any(CreateBidDto.class), anyString())) + .willThrow(new UserNotFoundException("해당 사용자를 찾을 수 없습니다.")); // when & then mockMvc.perform(post("/bids") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(bidRequest)) + .content(objectMapper.writeValueAsString(입찰_request)) ) .andExpectAll( status().isNotFound(), - jsonPath("$.message", is(userNotFoundException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 이미_종료된_경매_입찰시_400을_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateBidRequest bidRequest = new CreateBidRequest(1L, 10_000); - final InvalidAuctionToBidException invalidAuctionToBidException = new InvalidAuctionToBidException("이미 종료된 경매입니다"); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(bidService.create(any(CreateBidDto.class))) - .willThrow(invalidAuctionToBidException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(bidService.create(any(CreateBidDto.class), anyString())) + .willThrow(new InvalidAuctionToBidException("이미 종료된 경매입니다")); // when & then mockMvc.perform(post("/bids") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(bidRequest)) + .content(objectMapper.writeValueAsString(입찰_request)) ) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is(invalidAuctionToBidException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 이미_삭제된_경매_입찰시_400을_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateBidRequest bidRequest = new CreateBidRequest(1L, 10_000); - final InvalidAuctionToBidException invalidAuctionToBidException = new InvalidAuctionToBidException("삭제된 경매입니다"); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(bidService.create(any(CreateBidDto.class))) - .willThrow(invalidAuctionToBidException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(bidService.create(any(CreateBidDto.class), anyString())) + .willThrow(new InvalidAuctionToBidException("삭제된 경매입니다")); // when & then mockMvc.perform(post("/bids") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(bidRequest)) + .content(objectMapper.writeValueAsString(입찰_request)) ) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is(invalidAuctionToBidException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 판매자가_본인_경매에_입찰시_400을_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateBidRequest bidRequest = new CreateBidRequest(1L, 10_000); - final InvalidBidderException invalidBidderException = new InvalidBidderException("판매자는 입찰할 수 없습니다"); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(bidService.create(any(CreateBidDto.class))) - .willThrow(invalidBidderException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(bidService.create(any(CreateBidDto.class), anyString())) + .willThrow(new InvalidBidderException("판매자는 입찰할 수 없습니다")); // when & then mockMvc.perform(post("/bids") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(bidRequest)) + .content(objectMapper.writeValueAsString(입찰_request)) ) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is(invalidBidderException.getMessage())) + jsonPath("$.message").exists() ); } @Test - void 첫_입찰자가_시작가_낮은_금액으로_입찰시_400을_반환한다() throws Exception { + void 첫_입찰자가_시작가보다_낮은_금액으로_입찰시_400을_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateBidRequest bidRequest = new CreateBidRequest(1L, 10_000); - final InvalidBidPriceException invalidBidPriceException = new InvalidBidPriceException("입찰 금액이 잘못되었습니다"); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(bidService.create(any(CreateBidDto.class))) - .willThrow(invalidBidPriceException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(bidService.create(any(CreateBidDto.class), anyString())) + .willThrow(new InvalidBidPriceException("입찰 금액이 잘못되었습니다")); // when & then mockMvc.perform(post("/bids") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(bidRequest))) + .content(objectMapper.writeValueAsString(입찰_request))) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is(invalidBidPriceException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 마지막_입찰자가_연속으로_입찰시_400을_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateBidRequest bidRequest = new CreateBidRequest(1L, 10_000); - final InvalidBidderException invalidBidderException = new InvalidBidderException("이미 최고 입찰자입니다"); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(bidService.create(any(CreateBidDto.class))) - .willThrow(invalidBidderException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(bidService.create(any(CreateBidDto.class), anyString())) + .willThrow(new InvalidBidderException("이미 최고 입찰자입니다")); // when & then mockMvc.perform(post("/bids") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(bidRequest)) + .content(objectMapper.writeValueAsString(입찰_request)) ) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is(invalidBidderException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 마지막_입찰액보다_낮은_금액으로_입찰시_400을_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateBidRequest bidRequest = new CreateBidRequest(1L, 10_000); - final InvalidBidPriceException invalidBidPriceException = new InvalidBidPriceException("가능 입찰액보다 낮은 금액을 입력했습니다"); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(bidService.create(any(CreateBidDto.class))) - .willThrow(invalidBidPriceException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(bidService.create(any(CreateBidDto.class), anyString())) + .willThrow(new InvalidBidPriceException("가능 입찰액보다 낮은 금액을 입력했습니다")); // when & then mockMvc.perform(post("/bids") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(bidRequest)) + .content(objectMapper.writeValueAsString(입찰_request)) ) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is(invalidBidPriceException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 최소_입찰_단위보다_낮은_금액으로_입찰시_400을_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateBidRequest bidRequest = new CreateBidRequest(1L, 10_000); - final InvalidBidPriceException invalidBidPriceException = new InvalidBidPriceException("가능 입찰액보다 낮은 금액을 입력했습니다"); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(bidService.create(any(CreateBidDto.class))) - .willThrow(invalidBidPriceException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(bidService.create(any(CreateBidDto.class), anyString())) + .willThrow(new InvalidBidPriceException("가능 입찰액보다 낮은 금액을 입력했습니다")); // when & then mockMvc.perform(post("/bids") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(bidRequest)) + .content(objectMapper.writeValueAsString(입찰_request)) ) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is(invalidBidPriceException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 범위_밖의_금액으로_입찰시_400을_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateBidRequest bidRequest = new CreateBidRequest(1L, 2_100_000_001); - final InvalidBidPriceException invalidBidPriceException = new InvalidBidPriceException("입찰 금액이 잘못되었습니다"); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(bidService.create(any(CreateBidDto.class))) - .willThrow(invalidBidPriceException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(bidService.create(any(CreateBidDto.class), anyString())) + .willThrow(new InvalidBidPriceException("입찰 금액이 잘못되었습니다")); // when & then mockMvc.perform(post("/bids") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(bidRequest)) + .content(objectMapper.writeValueAsString(입찰_request)) ) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is(invalidBidPriceException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 경매_아이디가_없는_경우_입찰시_400을_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateBidRequest bidRequest = new CreateBidRequest(null, 10_000); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); // when & then mockMvc.perform(post("/bids") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(bidRequest)) + .content(objectMapper.writeValueAsString(경매_아이디_없이_입찰_request)) ) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is("경매 아이디가 입력되지 않았습니다.")) + jsonPath("$.message").exists() ); } @ParameterizedTest - @ValueSource(longs = {-1L, 0L}) - void 경매_아이디가_양수가_아닌_값으로_입찰시_400을_반환한다(final Long auctionId) throws Exception { + @MethodSource("provideBidRequestWithNotPositiveAuctionId") + void 경매_아이디가_양수가_아닌_값으로_입찰시_400을_반환한다(final CreateBidRequest 경매_아이디가_양수가_아닌_입찰_request) throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateBidRequest bidRequest = new CreateBidRequest(auctionId, 10_000); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); // when & then mockMvc.perform(post("/bids") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(bidRequest)) + .content(objectMapper.writeValueAsString(경매_아이디가_양수가_아닌_입찰_request)) ) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is("경매 아이디는 양수입니다.")) + jsonPath("$.message").exists() ); } + private static Stream provideBidRequestWithNotPositiveAuctionId() { + return Stream.of(경매_아이디가_양수가_아닌_입찰_request1, 경매_아이디가_양수가_아닌_입찰_request2); + } + @Test void 입찰_금액이_없는_경우_입찰시_400을_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateBidRequest bidRequest = new CreateBidRequest(1L, null); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); // when & then mockMvc.perform(post("/bids") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(bidRequest)) + .content(objectMapper.writeValueAsString(입찰액_없이_입찰_request)) ) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is("입찰 금액이 입력되지 않았습니다.")) + jsonPath("$.message").exists() ); } @ParameterizedTest - @ValueSource(ints = {-1, 0}) - void 입찰_금액이_양수가_아닌_값으로_입찰시_400을_반환한다(final Integer bidPrice) throws Exception { + @MethodSource("provideBidRequestWithNotPositiveBidPrice") + void 입찰_금액이_양수가_아닌_값으로_입찰시_400을_반환한다(final CreateBidRequest 입찰액이_양수가_아닌_입찰_request) throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateBidRequest bidRequest = new CreateBidRequest(1L, bidPrice); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); // when & then mockMvc.perform(post("/bids") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(bidRequest)) + .content(objectMapper.writeValueAsString(입찰액이_양수가_아닌_입찰_request)) ) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is("입찰 금액은 양수입니다.")) + jsonPath("$.message").exists() ); } + private static Stream provideBidRequestWithNotPositiveBidPrice() { + return Stream.of(입찰액이_양수가_아닌_입찰_request1, 입찰액이_양수가_아닌_입찰_request2); + } + @Test void 특정_경매에_대한_입찰_목록을_조회한다() throws Exception { // given - final ReadBidDto bid1 = new ReadBidDto("사용자1", "이미지1", 10_000, LocalDateTime.now()); - final ReadBidDto bid2 = new ReadBidDto("사용자2", "이미지2", 12_000, LocalDateTime.now()); - - given(bidService.readAllByAuctionId(anyLong())).willReturn(List.of(bid1, bid2)); + given(bidService.readAllByAuctionId(anyLong())).willReturn(List.of(입찰_정보_dto1, 입찰_정보_dto2)); // when & then - mockMvc.perform(get("/bids/{auctionId}", 1L) - .contentType(MediaType.APPLICATION_JSON) - ) - .andExpectAll( - status().isOk(), - jsonPath("$.bids.[0].name", is(bid1.name())), - jsonPath("$.bids.[0].profileImage", is(bid1.profileImage())), - jsonPath("$.bids.[0].price", is(bid1.price())), - jsonPath("$.bids.[0].bidTime").exists(), - jsonPath("$.bids.[1].name", is(bid2.name())), - jsonPath("$.bids.[1].profileImage", is(bid2.profileImage())), - jsonPath("$.bids.[1].price", is(bid2.price())), - jsonPath("$.bids.[1].bidTime").exists() - ) - .andDo( - restDocs.document( - responseFields( - fieldWithPath("bids.[]").type(JsonFieldType.ARRAY).description("특정 경매의 모든 입찰 목록"), - fieldWithPath("bids.[].name").type(JsonFieldType.STRING).description("입찰한 사용자의 닉네임"), - fieldWithPath("bids.[].profileImage").type(JsonFieldType.STRING).description("입찰한 사용자의 프로필 이미지 URL"), - fieldWithPath("bids.[].price").type(JsonFieldType.NUMBER).description("입찰한 금액"), - fieldWithPath("bids.[].bidTime").type(JsonFieldType.STRING).description("입찰한 시간") - ) - ) - ); + final ResultActions resultActions = mockMvc.perform(get("/bids/{auctionId}", 조회하려는_경매_아이디) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpectAll( + status().isOk(), + jsonPath("$.bids.[0].name", is(입찰_정보_dto1.name())), + jsonPath("$.bids.[0].profileImage").exists(), + jsonPath("$.bids.[0].price", is(입찰_정보_dto1.price())), + jsonPath("$.bids.[0].bidTime").exists(), + jsonPath("$.bids.[1].name", is(입찰_정보_dto2.name())), + jsonPath("$.bids.[1].profileImage").exists(), + jsonPath("$.bids.[1].price", is(입찰_정보_dto2.price())), + jsonPath("$.bids.[1].bidTime").exists() + ); + + readAllByAuctionId_문서화(resultActions); } @Test void 존재하지_않는_경매에_대한_입찰_목록을_조회하는_경우_404를_반환한다() throws Exception { // given - final AuctionNotFoundException auctionNotFoundException = new AuctionNotFoundException("해당 경매를 찾을 수 없습니다."); - given(bidService.readAllByAuctionId(anyLong())) - .willThrow(auctionNotFoundException); + given(bidService.readAllByAuctionId(anyLong())).willThrow(new AuctionNotFoundException("해당 경매를 찾을 수 없습니다.")); // when & then - final Long invalidAuctionId = -999L; - mockMvc.perform(get("/bids/{auctionId}", invalidAuctionId) + mockMvc.perform(get("/bids/{auctionId}", 존재하지_않는_경매_아이디) .contentType(MediaType.APPLICATION_JSON) ) .andExpectAll( status().isNotFound(), - jsonPath("$.message", is(auctionNotFoundException.getMessage())) + jsonPath("$.message").exists() ); } + + private void create_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + requestFields( + fieldWithPath("auctionId").description("입찰할 경매 ID"), + fieldWithPath("bidPrice").description("입찰 금액") + ) + ) + ); + } + + private void readAllByAuctionId_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + responseFields( + fieldWithPath("bids.[]").type(JsonFieldType.ARRAY) + .description("특정 경매의 모든 입찰 목록"), + fieldWithPath("bids.[].name").type(JsonFieldType.STRING) + .description("입찰한 사용자의 닉네임"), + fieldWithPath("bids.[].profileImage").type(JsonFieldType.STRING) + .description("입찰한 사용자의 프로필 이미지 URL"), + fieldWithPath("bids.[].price").type(JsonFieldType.NUMBER) + .description("입찰한 금액"), + fieldWithPath("bids.[].bidTime").type(JsonFieldType.STRING) + .description("입찰한 시간") + ) + ) + ); + } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/bid/presentation/fixture/BidControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/bid/presentation/fixture/BidControllerFixture.java new file mode 100644 index 000000000..ad7939d84 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/bid/presentation/fixture/BidControllerFixture.java @@ -0,0 +1,31 @@ +package com.ddang.ddang.bid.presentation.fixture; + +import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims; +import com.ddang.ddang.bid.application.dto.ReadBidDto; +import com.ddang.ddang.bid.presentation.dto.request.CreateBidRequest; +import com.ddang.ddang.configuration.CommonControllerSliceTest; + +import java.time.LocalDateTime; + +@SuppressWarnings("NonAsciiCharacters") +public class BidControllerFixture extends CommonControllerSliceTest { + + protected PrivateClaims 사용자_ID_클레임 = new PrivateClaims(1L); + protected PrivateClaims 존재하지_않는_사용자_ID_클레임 = new PrivateClaims(-999L); + protected Long 생성된_입찰_아이디 = 1L; + protected String 액세스_토큰_값 = "Bearer accessToken"; + protected Long 조회하려는_경매_아이디 = -999L; + protected Long 존재하지_않는_경매_아이디 = -999L; + + protected CreateBidRequest 입찰_request = new CreateBidRequest(1L, 10_000); + protected CreateBidRequest 존재하지_않는_경매에_대한_입찰_request = new CreateBidRequest(1L, 10_000); + protected CreateBidRequest 경매_아이디_없이_입찰_request = new CreateBidRequest(null, 10_000); + protected static CreateBidRequest 경매_아이디가_양수가_아닌_입찰_request1 = new CreateBidRequest(-1L, 10_000); + protected static CreateBidRequest 경매_아이디가_양수가_아닌_입찰_request2 = new CreateBidRequest(0L, 10_000); + protected CreateBidRequest 입찰액_없이_입찰_request = new CreateBidRequest(1L, null); + protected static CreateBidRequest 입찰액이_양수가_아닌_입찰_request1 = new CreateBidRequest(1L, -1); + protected static CreateBidRequest 입찰액이_양수가_아닌_입찰_request2 = new CreateBidRequest(1L, 0); + + protected ReadBidDto 입찰_정보_dto1 = new ReadBidDto("사용자1", 1L, false, 10_000, LocalDateTime.now()); + protected ReadBidDto 입찰_정보_dto2 = new ReadBidDto("사용자2", 2L, false, 12_000, LocalDateTime.now()); +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/category/application/CategoryServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/category/application/CategoryServiceTest.java index 5fe284f5d..3f7c312ec 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/category/application/CategoryServiceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/category/application/CategoryServiceTest.java @@ -1,88 +1,60 @@ package com.ddang.ddang.category.application; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + import com.ddang.ddang.category.application.dto.ReadCategoryDto; import com.ddang.ddang.category.application.exception.CategoryNotFoundException; -import com.ddang.ddang.category.domain.Category; -import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.category.application.fixture.CategoryServiceFixture; +import com.ddang.ddang.configuration.IsolateDatabase; +import java.util.List; +import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@Transactional +@IsolateDatabase @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class CategoryServiceTest { +class CategoryServiceTest extends CategoryServiceFixture { @Autowired CategoryService categoryService; - @Autowired - JpaCategoryRepository categoryRepository; - @Test void 모든_메인_카테고리를_조회한다() { - // given - final Category main1 = new Category("main1"); - final Category main2 = new Category("main2"); - final Category sub = new Category("sub"); - - main1.addSubCategory(sub); - - categoryRepository.save(main1); - categoryRepository.save(main2); - // when final List actual = categoryService.readAllMain(); // then - assertThat(actual).hasSize(2); - } - - @Test - void 메인_카테고리가_없는_경우_메인_카테고리_조회시_예외가_발생한다() { - // when & then - assertThatThrownBy(() -> categoryService.readAllMain()) - .isInstanceOf(CategoryNotFoundException.class) - .hasMessage("등록된 메인 카테고리가 없습니다."); + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(2); + softAssertions.assertThat(actual.get(0).id()).isEqualTo(가구_카테고리.getId()); + softAssertions.assertThat(actual.get(0).name()).isEqualTo(가구_카테고리.getName()); + softAssertions.assertThat(actual.get(1).id()).isEqualTo(전자기기_카테고리.getId()); + softAssertions.assertThat(actual.get(1).name()).isEqualTo(전자기기_카테고리.getName()); + }); } @Test void 메인_카테고리에_해당하는_모든_서브_카테고리를_조회한다() { - // given - final Category main = new Category("main"); - final Category sub1 = new Category("sub1"); - final Category sub2 = new Category("sub2"); - - main.addSubCategory(sub1); - main.addSubCategory(sub2); - - categoryRepository.save(main); - // when - final List actual = categoryService.readAllSubByMainId(main.getId()); + final List actual = categoryService.readAllSubByMainId(가구_카테고리.getId()); // then - assertThat(actual).hasSize(2); + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(2); + softAssertions.assertThat(actual.get(0).id()).isEqualTo(가구_서브_의자_카테고리.getId()); + softAssertions.assertThat(actual.get(0).name()).isEqualTo(가구_서브_의자_카테고리.getName()); + softAssertions.assertThat(actual.get(1).id()).isEqualTo(가구_서브_책상_카테고리.getId()); + softAssertions.assertThat(actual.get(1).name()).isEqualTo(가구_서브_책상_카테고리.getName()); + }); } @Test void 지정한_메인_카테고리에_해당하는_서브_카테고리가_없는_경우_서브_카테고리_조회시_예외가_발생한다() { - // given - final Category main = new Category("main"); - - categoryRepository.save(main); - // when & then - assertThatThrownBy(() -> categoryService.readAllSubByMainId(main.getId())) + assertThatThrownBy(() -> categoryService.readAllSubByMainId(전자기기_카테고리.getId())) .isInstanceOf(CategoryNotFoundException.class) .hasMessage("지정한 메인 카테고리에 해당하는 서브 카테고리가 없습니다."); } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/category/application/fixture/CategoryServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/category/application/fixture/CategoryServiceFixture.java new file mode 100644 index 000000000..ead6f5bd1 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/category/application/fixture/CategoryServiceFixture.java @@ -0,0 +1,32 @@ +package com.ddang.ddang.category.application.fixture; + +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +@SuppressWarnings("NonAsciiCharacters") +public class CategoryServiceFixture { + + @Autowired + private JpaCategoryRepository categoryRepository; + + protected Category 가구_카테고리; + protected Category 전자기기_카테고리; + protected Category 가구_서브_의자_카테고리; + protected Category 가구_서브_책상_카테고리; + + @BeforeEach + void setUp() { + 가구_카테고리 = new Category("가구"); + 전자기기_카테고리 = new Category("전자기기"); + 가구_서브_의자_카테고리 = new Category("의자"); + 가구_서브_책상_카테고리 = new Category("책상"); + + 가구_카테고리.addSubCategory(가구_서브_의자_카테고리); + 가구_카테고리.addSubCategory(가구_서브_책상_카테고리); + + categoryRepository.save(가구_카테고리); + categoryRepository.save(전자기기_카테고리); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/category/domain/CategoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/category/domain/CategoryTest.java index b6b475bf1..28a48507d 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/category/domain/CategoryTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/category/domain/CategoryTest.java @@ -12,18 +12,17 @@ class CategoryTest { @Test void 카테고리_연관_관계를_세팅한다() { // given - Category main = new Category("main"); - Category sub = new Category("sub"); + final Category main = new Category("main"); + final Category sub = new Category("sub"); // when main.addSubCategory(sub); // then SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(main.getSubCategories()) - .hasSize(1); - softAssertions.assertThat(sub.getMainCategory()) - .isNotNull(); + softAssertions.assertThat(main.getSubCategories()).hasSize(1); + softAssertions.assertThat(main.getSubCategories().get(0).getName()).isEqualTo(sub.getName()); + softAssertions.assertThat(sub.getMainCategory().getName()).isEqualTo(main.getName()); }); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/category/infrastructure/persistence/JpaCategoryRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/category/infrastructure/persistence/JpaCategoryRepositoryTest.java index 4ef5d577a..74541c3f4 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/category/infrastructure/persistence/JpaCategoryRepositoryTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/category/infrastructure/persistence/JpaCategoryRepositoryTest.java @@ -3,9 +3,8 @@ import static org.assertj.core.api.Assertions.assertThat; import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.fixture.JpaCategoryRepositoryFixture; import com.ddang.ddang.configuration.QuerydslConfiguration; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; import java.util.List; import java.util.Optional; import org.assertj.core.api.SoftAssertions; @@ -17,103 +16,53 @@ import org.springframework.context.annotation.Import; @DataJpaTest +@Import(QuerydslConfiguration.class) @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -@Import(QuerydslConfiguration.class) -class JpaCategoryRepositoryTest { - - @PersistenceContext - EntityManager em; +class JpaCategoryRepositoryTest extends JpaCategoryRepositoryFixture { @Autowired JpaCategoryRepository categoryRepository; @Test void 모든_메인_카테고리를_조회한다() { - // given - final Category main1 = new Category("main1"); - final Category main2 = new Category("main2"); - final Category sub = new Category("sub"); - - main1.addSubCategory(sub); - - categoryRepository.save(main1); - categoryRepository.save(main2); - - em.flush(); - em.clear(); - // when final List actual = categoryRepository.findMainAllByMainCategoryIsNull(); // then - assertThat(actual).hasSize(2); + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(2); + softAssertions.assertThat(actual.get(0)).isEqualTo(가구_카테고리); + softAssertions.assertThat(actual.get(1)).isEqualTo(전자기기_카테고리); + }); } @Test void 메인_카테고리에_해당하는_모든_서브_카테고리를_조회한다() { - // given - final Category main = new Category("main"); - final Category sub1 = new Category("sub1"); - final Category sub2 = new Category("sub2"); - - main.addSubCategory(sub1); - main.addSubCategory(sub2); - - categoryRepository.save(main); - - em.flush(); - em.clear(); - // when - final List actual = categoryRepository.findSubAllByMainCategoryId(main.getId()); + final List actual = categoryRepository.findSubAllByMainCategoryId(가구_카테고리.getId()); // then - assertThat(actual).hasSize(2); + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(2); + softAssertions.assertThat(actual.get(0)).isEqualTo(가구_서브_의자_카테고리); + softAssertions.assertThat(actual.get(1)).isEqualTo(가구_서브_책상_카테고리); + }); } @Test - void 하위_카테고리를_조회한다() { - // given - final Category main = new Category("main"); - final Category sub1 = new Category("sub1"); - final Category sub2 = new Category("sub2"); - - main.addSubCategory(sub1); - main.addSubCategory(sub2); - - categoryRepository.save(main); - - em.flush(); - em.clear(); - + void 서브_카테고리를_조회한다() { // when - final Optional actual = categoryRepository.findSubCategoryById(sub1.getId()); + final Optional actual = categoryRepository.findSubCategoryById(가구_서브_의자_카테고리.getId()); // then - SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual).isPresent(); - softAssertions.assertThat(actual.get()).isEqualTo(sub1); - }); + assertThat(actual).contains(가구_서브_의자_카테고리); } @Test - void 하위_카테고리가_아닌_아이디를_전달하면_빈_Optional을_반환한다() { - // given - final Category main = new Category("main"); - final Category sub1 = new Category("sub1"); - final Category sub2 = new Category("sub2"); - - main.addSubCategory(sub1); - main.addSubCategory(sub2); - - categoryRepository.save(main); - - em.flush(); - em.clear(); - + void 서브_카테고리가_아닌_카테고리의_아이디를_전달하면_빈_Optional을_반환한다() { // when - final Optional actual = categoryRepository.findSubCategoryById(main.getId()); + final Optional actual = categoryRepository.findSubCategoryById(가구_카테고리.getId()); // then assertThat(actual).isEmpty(); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/category/infrastructure/persistence/fixture/JpaCategoryRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/category/infrastructure/persistence/fixture/JpaCategoryRepositoryFixture.java new file mode 100644 index 000000000..f08ef1029 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/category/infrastructure/persistence/fixture/JpaCategoryRepositoryFixture.java @@ -0,0 +1,40 @@ +package com.ddang.ddang.category.infrastructure.persistence.fixture; + +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +@SuppressWarnings("NonAsciiCharacters") +public class JpaCategoryRepositoryFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaCategoryRepository categoryRepository; + + protected Category 가구_카테고리; + protected Category 전자기기_카테고리; + protected Category 가구_서브_의자_카테고리; + protected Category 가구_서브_책상_카테고리; + + @BeforeEach + void setUp() { + 가구_카테고리 = new Category("가구"); + 전자기기_카테고리 = new Category("전자기기"); + 가구_서브_의자_카테고리 = new Category("의자"); + 가구_서브_책상_카테고리 = new Category("책상"); + + 가구_카테고리.addSubCategory(가구_서브_의자_카테고리); + 가구_카테고리.addSubCategory(가구_서브_책상_카테고리); + + categoryRepository.save(가구_카테고리); + categoryRepository.save(전자기기_카테고리); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/category/presentation/CategoryControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/category/presentation/CategoryControllerTest.java index 760de82e8..48e0098f8 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/category/presentation/CategoryControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/category/presentation/CategoryControllerTest.java @@ -9,57 +9,26 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.ddang.ddang.category.application.CategoryService; import com.ddang.ddang.category.application.dto.ReadCategoryDto; import com.ddang.ddang.category.application.exception.CategoryNotFoundException; -import com.ddang.ddang.configuration.RestDocsConfiguration; +import com.ddang.ddang.configuration.CommonControllerSliceTest; import com.ddang.ddang.exception.GlobalExceptionHandler; import java.util.List; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; -import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@WebMvcTest(controllers = {CategoryController.class}, - excludeFilters = { - @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfigurer.class), - @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com\\.ddang\\.ddang\\.authentication\\.configuration\\..*") - } -) -@AutoConfigureRestDocs -@Import(RestDocsConfiguration.class) -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -@SuppressWarnings("NonAsciiCharacters") -class CategoryControllerTest { - - @MockBean - CategoryService categoryService; - @Autowired - CategoryController categoryController; - - @Autowired - RestDocumentationResultHandler restDocs; +@SuppressWarnings("NonAsciiCharacters") +class CategoryControllerTest extends CommonControllerSliceTest { MockMvc mockMvc; @BeforeEach - void setUp(@Autowired RestDocumentationContextProvider provider) { + void setUp() { mockMvc = MockMvcBuilders.standaloneSetup(categoryController) .setControllerAdvice(new GlobalExceptionHandler()) .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/ChatRoomServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/ChatRoomServiceTest.java index 6d2e073ae..4ce53a7a7 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/ChatRoomServiceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/ChatRoomServiceTest.java @@ -1,37 +1,22 @@ package com.ddang.ddang.chat.application; +import com.ddang.ddang.auction.application.dto.ReadChatRoomDto; import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; -import com.ddang.ddang.auction.domain.Auction; -import com.ddang.ddang.auction.domain.BidUnit; -import com.ddang.ddang.auction.domain.Price; import com.ddang.ddang.auction.domain.exception.WinnerNotFoundException; -import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; -import com.ddang.ddang.bid.domain.Bid; -import com.ddang.ddang.bid.domain.BidPrice; -import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository; -import com.ddang.ddang.category.domain.Category; -import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; -import com.ddang.ddang.chat.application.dto.CreateChatRoomDto; import com.ddang.ddang.chat.application.dto.ReadChatRoomWithLastMessageDto; import com.ddang.ddang.chat.application.dto.ReadParticipatingChatRoomDto; import com.ddang.ddang.chat.application.exception.ChatRoomNotFoundException; import com.ddang.ddang.chat.application.exception.InvalidAuctionToChatException; -import com.ddang.ddang.chat.application.exception.UserNotAccessibleException; -import com.ddang.ddang.chat.domain.ChatRoom; -import com.ddang.ddang.chat.domain.Message; -import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; -import com.ddang.ddang.chat.infrastructure.persistence.JpaMessageRepository; +import com.ddang.ddang.chat.application.exception.InvalidUserToChat; +import com.ddang.ddang.chat.application.fixture.ChatRoomServiceFixture; import com.ddang.ddang.configuration.IsolateDatabase; import com.ddang.ddang.user.application.exception.UserNotFoundException; -import com.ddang.ddang.user.domain.User; -import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import java.time.LocalDateTime; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -40,74 +25,15 @@ @IsolateDatabase @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class ChatRoomServiceTest { +class ChatRoomServiceTest extends ChatRoomServiceFixture { @Autowired ChatRoomService chatRoomService; - @Autowired - JpaChatRoomRepository chatRoomRepository; - - @Autowired - JpaMessageRepository messageRepository; - - @Autowired - JpaUserRepository userRepository; - - @Autowired - JpaCategoryRepository categoryRepository; - - @Autowired - JpaAuctionRepository auctionRepository; - - @Autowired - JpaBidRepository bidRepository; - @Test void 채팅방을_생성한다() { - // given - final Category main = new Category("메인"); - final Category sub = new Category("서브"); - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User buyer = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - userRepository.save(seller); - userRepository.save(buyer); - - final Auction auction = Auction.builder() - .title("경매") - .description("설명") - .seller(seller) - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(10_000)) - .closingTime(LocalDateTime.now().minusDays(3L)) - .subCategory(sub) - .build(); - auctionRepository.save(auction); - - final Bid bid = new Bid(auction, buyer, new BidPrice(15_000)); - bidRepository.save(bid); - - auction.updateLastBid(bid); - - final Long auctionId = auction.getId(); - final Long userId = buyer.getId(); - final CreateChatRoomDto createChatRoomDto = new CreateChatRoomDto(auctionId); - // when - final Long actual = chatRoomService.create(userId, createChatRoomDto); + final Long actual = chatRoomService.create(구매자.getId(), 채팅방_생성을_위한_DTO); // then assertThat(actual).isPositive(); @@ -115,482 +41,87 @@ class ChatRoomServiceTest { @Test void 채팅방_생성시_요청한_사용자_정보를_찾을_수_없다면_예외가_발생한다() { - // given - final Category main = new Category("메인"); - final Category sub = new Category("서브"); - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User buyer = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - userRepository.save(seller); - userRepository.save(buyer); - - final Auction auction = Auction.builder() - .title("경매") - .description("설명") - .seller(seller) - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(10_000)) - .closingTime(LocalDateTime.now().minusDays(3L)) - .subCategory(sub) - .build(); - auctionRepository.save(auction); - - final Bid bid = new Bid(auction, buyer, new BidPrice(15_000)); - bidRepository.save(bid); - - auction.updateLastBid(bid); - - final Long auctionId = auction.getId(); - final Long invalidUserId = -999L; - final CreateChatRoomDto createChatRoomDto = new CreateChatRoomDto(auctionId); - // when & then - assertThatThrownBy(() -> chatRoomService.create(invalidUserId, createChatRoomDto)) + assertThatThrownBy(() -> chatRoomService.create(존재하지_않는_사용자_아이디, 채팅방_생성을_위한_DTO)) .isInstanceOf(UserNotFoundException.class) .hasMessage("사용자 정보를 찾을 수 없습니다."); } @Test void 채팅방_생성시_관련된_경매_정보를_찾을_수_없다면_예외가_발생한다() { - // given - final User user = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - userRepository.save(user); - - final Long userId = user.getId(); - final Long invalidAuctionId = -999L; - final CreateChatRoomDto invalidDto = new CreateChatRoomDto(invalidAuctionId); - - // when & then - assertThatThrownBy(() -> chatRoomService.create(userId, invalidDto)) + assertThatThrownBy(() -> chatRoomService.create(구매자.getId(), 경매_정보가_없어서_채팅방을_생성할_수_없는_DTO)) .isInstanceOf(AuctionNotFoundException.class) .hasMessage("해당 경매를 찾을 수 없습니다."); } @Test void 경매가_종료되지_않은_상태에서_채팅방을_생성하면_예외가_발생한다() { - // given - final Category main = new Category("메인"); - final Category sub = new Category("서브"); - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User buyer = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - userRepository.save(seller); - userRepository.save(buyer); - - final Auction auction = Auction.builder() - .title("경매") - .description("설명") - .seller(seller) - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(10_000)) - .closingTime(LocalDateTime.now().plusDays(3L)) - .subCategory(sub) - .build(); - auctionRepository.save(auction); - - final Bid bid = new Bid(auction, buyer, new BidPrice(15_000)); - bidRepository.save(bid); - - auction.updateLastBid(bid); - - final Long auctionId = auction.getId(); - final Long userId = buyer.getId(); - final CreateChatRoomDto createChatRoomDto = new CreateChatRoomDto(auctionId); - // when & then - assertThatThrownBy(() -> chatRoomService.create(userId, createChatRoomDto)) + assertThatThrownBy(() -> chatRoomService.create(판매자.getId(), 경매가_진행중이라서_채팅방을_생성할_수_없는_DTO)) .isInstanceOf(InvalidAuctionToChatException.class) .hasMessage("경매가 아직 종료되지 않았습니다."); } - @Test - void 경매가_삭제된_상태에서_채팅방을_생성하면_예외가_발생한다() { - // given - final Category main = new Category("메인"); - final Category sub = new Category("서브"); - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User buyer = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - userRepository.save(seller); - userRepository.save(buyer); - - final Auction auction = Auction.builder() - .title("경매") - .description("설명") - .seller(seller) - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(10_000)) - .closingTime(LocalDateTime.now().minusDays(3L)) - .subCategory(sub) - .build(); - auctionRepository.save(auction); - - auction.delete(); - - final Long auctionId = auction.getId(); - final Long userId = buyer.getId(); - final CreateChatRoomDto createChatRoomDto = new CreateChatRoomDto(auctionId); - - // when & then - assertThatThrownBy(() -> chatRoomService.create(userId, createChatRoomDto)) - .isInstanceOf(InvalidAuctionToChatException.class) - .hasMessage("삭제된 경매입니다."); - } - @Test void 낙찰자가_없는데_채팅방을_생성하면_예외가_발생한다() { - // given - final Category main = new Category("메인"); - final Category sub = new Category("서브"); - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - userRepository.save(seller); - - final Auction auction = Auction.builder() - .title("경매") - .description("설명") - .seller(seller) - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(10_000)) - .closingTime(LocalDateTime.now().minusDays(3L)) - .subCategory(sub) - .build(); - auctionRepository.save(auction); - - final Long auctionId = auction.getId(); - final Long userId = seller.getId(); - final CreateChatRoomDto createChatRoomDto = new CreateChatRoomDto(auctionId); - // when & then - assertThatThrownBy(() -> chatRoomService.create(userId, createChatRoomDto)) + assertThatThrownBy(() -> chatRoomService.create(판매자.getId(), 낙찰자가_없어서_채팅방을_생성할_수_없는_DTO)) .isInstanceOf(WinnerNotFoundException.class) .hasMessage("낙찰자가 존재하지 않습니다"); } @Test void 채팅방_생성을_요청한_사용자가_경매의_판매자_또는_최종_낙찰자가_아니라면_예외가_발생한다() { - // given - final Category main = new Category("메인"); - final Category sub = new Category("서브"); - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User buyer = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - final User stranger = User.builder() - .name("회원3") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12347") - .build(); - userRepository.save(seller); - userRepository.save(buyer); - userRepository.save(stranger); - - final Auction auction = Auction.builder() - .title("경매") - .description("설명") - .seller(seller) - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(10_000)) - .closingTime(LocalDateTime.now().minusDays(3L)) - .subCategory(sub) - .build(); - auctionRepository.save(auction); - - final Bid bid = new Bid(auction, buyer, new BidPrice(15_000)); - bidRepository.save(bid); - - auction.updateLastBid(bid); - - final Long auctionId = auction.getId(); - final Long strangeUserId = stranger.getId(); - final CreateChatRoomDto createChatRoomDto = new CreateChatRoomDto(auctionId); - // when & then - assertThatThrownBy(() -> chatRoomService.create(strangeUserId, createChatRoomDto)) - .isInstanceOf(UserNotAccessibleException.class) + assertThatThrownBy(() -> chatRoomService.create(경매에_참여한_적_없는_사용자.getId(), 채팅방_생성을_위한_DTO)) + .isInstanceOf(InvalidUserToChat.class) .hasMessage("경매의 판매자 또는 최종 낙찰자만 채팅이 가능합니다."); } @Test void 해당_경매에_대한_채팅이_이미_존재할_경우_존재하는_채팅방의_아이디를_반환한다() { - // given - final Category main = new Category("메인"); - final Category sub = new Category("서브"); - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User buyer = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - userRepository.save(seller); - userRepository.save(buyer); - - final Auction auction = Auction.builder() - .title("경매") - .description("설명") - .seller(seller) - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(10_000)) - .closingTime(LocalDateTime.now().minusDays(3L)) - .subCategory(sub) - .build(); - auctionRepository.save(auction); - - final Bid bid = new Bid(auction, buyer, new BidPrice(15_000)); - bidRepository.save(bid); - - auction.updateLastBid(bid); - - final Long auctionId = auction.getId(); - final Long userId = buyer.getId(); - final CreateChatRoomDto createChatRoomDto = new CreateChatRoomDto(auctionId); - - final ChatRoom persistChatRoom = new ChatRoom(auction, auction.findWinner(LocalDateTime.now()).get()); - chatRoomRepository.save(persistChatRoom); - - final Long expect = persistChatRoom.getId(); - // when - final Long actual = chatRoomService.create(userId, createChatRoomDto); + final Long actual = chatRoomService.create(엔초.getId(), 엔초_지토_채팅방_생성을_위한_DTO); // then - assertThat(actual).isEqualTo(expect); + assertThat(actual).isEqualTo(엔초_지토_채팅방.getId()); } @Test void 사용자가_참여한_모든_채팅방을_마지막에_전송된_메시지와_함께_조회하며_마지막_메시지가_최근인_순서로_정렬하여_조회한다() { - // given - final Category main = new Category("메인"); - final Category sub = new Category("서브"); - main.addSubCategory(sub); - categoryRepository.save(main); - - final User merry = User.builder().name("메리").profileImage("profile.png").reliability(4.7d).oauthId("12345") - .build(); - final User encho = User.builder().name("엔초").profileImage("profile.png").reliability(4.7d).oauthId("12346") - .build(); - final User jamie = User.builder().name("제이미").profileImage("profile.png").reliability(4.7d).oauthId("12347") - .build(); - final User zeeto = User.builder().name("지토").profileImage("profile.png").reliability(4.7d).oauthId("12348") - .build(); - userRepository.save(merry); - userRepository.save(encho); - userRepository.save(jamie); - userRepository.save(zeeto); - - final Auction merryAuction = Auction.builder() - .title("경매 1") - .seller(merry) - .subCategory(sub) - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(10_000)) - .build(); - final Auction enchoAuction = Auction.builder() - .title("경매 2") - .seller(encho) - .subCategory(sub) - .bidUnit(new BidUnit(2_000)) - .startPrice(new Price(20_000)) - .build(); - final Auction jamieAuction = Auction.builder() - .title("경매 3") - .seller(jamie) - .subCategory(sub) - .bidUnit(new BidUnit(3_000)) - .startPrice(new Price(30_000)) - .build(); - - auctionRepository.save(merryAuction); - auctionRepository.save(enchoAuction); - auctionRepository.save(jamieAuction); - - final ChatRoom merryZeeto = new ChatRoom(merryAuction, zeeto); - final ChatRoom enchoZeeto = new ChatRoom(enchoAuction, zeeto); - final ChatRoom jamieEncho = new ChatRoom(jamieAuction, encho); - chatRoomRepository.save(merryZeeto); - chatRoomRepository.save(enchoZeeto); - chatRoomRepository.save(jamieEncho); - - final Message message1 = Message.builder() - .chatRoom(jamieEncho) - .contents("jamieEncho message 1") - .writer(jamie) - .receiver(encho) - .build(); - messageRepository.save(message1); - final Message lastMessage1 = Message.builder() - .chatRoom(enchoZeeto) - .writer(encho) - .receiver(zeeto) - .contents("enchoZeeto message 1") - .build(); - messageRepository.save(lastMessage1); - final Message lastMessage2 = Message.builder() - .chatRoom(jamieEncho) - .contents("jamieEncho message 2") - .writer(jamie) - .receiver(encho) - .build(); - messageRepository.save(lastMessage2); - // when - final List actual = chatRoomService.readAllByUserId(encho.getId()); + final List actual = chatRoomService.readAllByUserId(엔초.getId()); // then SoftAssertions.assertSoftly(softAssertions -> { softAssertions.assertThat(actual).hasSize(2); - softAssertions.assertThat(actual.get(0).id()).isEqualTo(jamieEncho.getId()); - softAssertions.assertThat(actual.get(0).auctionDto().id()).isEqualTo(jamieEncho.getAuction().getId()); - softAssertions.assertThat(actual.get(0).partnerDto().id()).isEqualTo(jamie.getId()); - softAssertions.assertThat(actual.get(0).lastMessageDto().id()).isEqualTo(lastMessage2.getId()); - softAssertions.assertThat(actual.get(1).id()).isEqualTo(enchoZeeto.getId()); - softAssertions.assertThat(actual.get(1).auctionDto().id()).isEqualTo(enchoZeeto.getAuction().getId()); - softAssertions.assertThat(actual.get(1).partnerDto().id()).isEqualTo(zeeto.getId()); - softAssertions.assertThat(actual.get(1).lastMessageDto().id()).isEqualTo(lastMessage1.getId()); + softAssertions.assertThat(actual.get(0)).isEqualTo(엔초_채팅_목록의_제이미_엔초_채팅방_정보); + softAssertions.assertThat(actual.get(1)).isEqualTo(엔초_채팅_목록의_엔초_지토_채팅방_정보); }); } @Test void 사용자가_참여한_채팅방_목록_조회시_사용자_정보를_찾을_수_없다면_예외가_발생한다() { - // given - final Long invalidUserId = -999L; - // when & then - assertThatThrownBy(() -> chatRoomService.readAllByUserId(invalidUserId)) + assertThatThrownBy(() -> chatRoomService.readAllByUserId(존재하지_않는_사용자_아이디)) .isInstanceOf(UserNotFoundException.class) .hasMessageContaining("사용자 정보를 찾을 수 없습니다."); } @Test void 지정한_아이디에_해당하는_채팅방을_조회한다() { - // given - final User seller = User.builder().name("판매자").profileImage("profile.png").reliability(4.7d).oauthId("12345") - .build(); - final User buyer = User.builder().name("구매자").profileImage("profile.png").reliability(4.7d).oauthId("12346") - .build(); - userRepository.save(seller); - userRepository.save(buyer); - - final Category main = new Category("main"); - final Category sub = new Category("sub"); - main.addSubCategory(sub); - categoryRepository.save(main); - - final Auction auction = Auction.builder(). - title("경매") - .seller(seller) - .subCategory(sub) - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(10_000)) - .closingTime(LocalDateTime.now().minusDays(6)) - .build(); - auctionRepository.save(auction); - final Bid bid = new Bid(auction, buyer, new BidPrice(15_000)); - bidRepository.save(bid); - auction.updateLastBid(bid); - - final ChatRoom chatRoom = new ChatRoom(auction, buyer); - chatRoomRepository.save(chatRoom); - - final ReadParticipatingChatRoomDto expect = ReadParticipatingChatRoomDto.of(seller, chatRoom, LocalDateTime.now()); - // when - final ReadParticipatingChatRoomDto actual = chatRoomService.readByChatRoomId(chatRoom.getId(), seller.getId()); + final ReadParticipatingChatRoomDto actual = chatRoomService.readByChatRoomId(엔초_지토_채팅방.getId(), 엔초.getId()); // then - assertThat(actual).isEqualTo(expect); + assertThat(actual).isEqualTo(엔초가_조회한_엔초_지토_채팅방_정보_조회_결과); } @Test void 지정한_아이디에_해당하는_채팅방_조회시_조회를_요청한_사용자의_정보를_찾을_수_없다면_예외가_발생한다() { - // given - final User user = User.builder().name("구매자").profileImage("profile.png").reliability(4.7d).oauthId("12345") - .build(); - userRepository.save(user); - - final Auction auction = Auction.builder() - .title("경매") - .build(); - auctionRepository.save(auction); - - final ChatRoom chatRoom = new ChatRoom(auction, user); - chatRoomRepository.save(chatRoom); - - final Long chatRoomId = chatRoom.getId(); - final Long invalidUserId = -999L; - // when & then - assertThatThrownBy(() -> chatRoomService.readByChatRoomId(chatRoomId, invalidUserId)) + assertThatThrownBy(() -> chatRoomService.readByChatRoomId(엔초_지토_채팅방.getId(), 존재하지_않는_사용자_아이디)) .isInstanceOf(UserNotFoundException.class) .hasMessageContaining("사용자 정보를 찾을 수 없습니다."); @@ -598,16 +129,8 @@ class ChatRoomServiceTest { @Test void 지정한_아이디에_해당하는_채팅방을_찾을_수_없다면_예외가_발생한다() { - // given - final User user = User.builder().name("구매자").profileImage("profile.png").reliability(4.7d).oauthId("12345") - .build(); - userRepository.save(user); - - final Long invalidChatRoomId = -999L; - final Long userId = user.getId(); - // when & then - assertThatThrownBy(() -> chatRoomService.readByChatRoomId(invalidChatRoomId, userId)) + assertThatThrownBy(() -> chatRoomService.readByChatRoomId(존재하지_않는_채팅방_아이디, 엔초.getId())) .isInstanceOf(ChatRoomNotFoundException.class) .hasMessageContaining("지정한 아이디에 대한 채팅방을 찾을 수 없습니다."); @@ -615,29 +138,53 @@ class ChatRoomServiceTest { @Test void 지정한_아이디에_해당하는_채팅방_조회시_주어진_사용자가_채팅의_참여자가_아니라면_예외가_발생한다() { - // given - final User seller = User.builder().name("판매자").profileImage("profile.png").reliability(4.7d).oauthId("12345") - .build(); - final User buyer = User.builder().name("구매자").profileImage("profile.png").reliability(4.7d).oauthId("12346") - .build(); - final User stranger = User.builder().name("일반인").profileImage("profile.png").reliability(4.7d).oauthId("12347") - .build(); - userRepository.save(seller); - userRepository.save(buyer); - userRepository.save(stranger); - - final Auction auction = Auction.builder().title("경매").seller(seller).build(); - auctionRepository.save(auction); - - final ChatRoom chatRoom = new ChatRoom(auction, buyer); - chatRoomRepository.save(chatRoom); - - final Long chatRoomId = chatRoom.getId(); - final Long nonAuthorizedUserId = stranger.getId(); - // when & then - assertThatThrownBy(() -> chatRoomService.readByChatRoomId(chatRoomId, nonAuthorizedUserId)) - .isInstanceOf(UserNotAccessibleException.class) + assertThatThrownBy(() -> chatRoomService.readByChatRoomId(엔초_지토_채팅방.getId(), 제이미.getId())) + .isInstanceOf(InvalidUserToChat.class) .hasMessageContaining("해당 채팅방에 접근할 권한이 없습니다."); } + + @Test + void 지정한_경매_아이디와_관련된_채팅방_정보를_조회할_때_조회한_사람이_해당_채팅방_참여자라면_채팅방_아이디와_참여가능여부_참을_반환한다() { + // when + final ReadChatRoomDto actual = chatRoomService.readChatInfoByAuctionId(판매자_엔초_구매자_지토_경매.getId(), 엔초_회원_정보); + + // then + assertThat(actual).isEqualTo(엔초_지토_채팅방_정보_및_참여_가능); + } + + @Test + void 지정한_경매_아이디와_관련된_채팅방_정보를_조회할_때_조회한_사람이_해당_채팅방_참여자가_아니라면_채팅방_아이디와_참여가능여부_거짓을_반환한다() { + // when + final ReadChatRoomDto actual = + chatRoomService.readChatInfoByAuctionId(판매자_엔초_구매자_지토_경매.getId(), 경매에_참여한_적_없는_사용자_정보); + + // then + assertThat(actual).isEqualTo(엔초_지토_채팅방_정보_및_참여_불가능); + } + + @Test + void 지정한_경매_아이디와_관련된_채팅방을_조회할_때_조회를_요청한_사용자_정보를_찾을_수_없다면_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> chatRoomService.readChatInfoByAuctionId(판매자_엔초_구매자_지토_경매.getId(), 존재하지_않는_사용자_정보)) + .isInstanceOf(UserNotFoundException.class) + .hasMessage("회원 정보를 찾을 수 없습니다."); + } + + @Test + void 지정한_경매_아이디와_관련된_채팅방을_조회할_때_경매를_찾을_수_없다면_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> chatRoomService.readChatInfoByAuctionId(존재하지_않는_경매_아이디, 엔초_회원_정보)) + .isInstanceOf(AuctionNotFoundException.class) + .hasMessage("지정한 아이디에 대한 경매를 찾을 수 없습니다."); + } + + @Test + void 지정한_경매_아이디와_관련된_채팅방을_조회할_때_채팅방을_찾을_수_없다면_채팅방_아이디_null과_참여가능여부를_반환한다() { + // when + final ReadChatRoomDto actual = chatRoomService.readChatInfoByAuctionId(채팅방이_없는_경매.getId(), 판매자_회원_정보); + + // then + assertThat(actual).isEqualTo(채팅방은_아직_없지만_참여_가능); + } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/MessageServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/MessageServiceTest.java index 1940d517d..a65cadb92 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/MessageServiceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/MessageServiceTest.java @@ -1,662 +1,151 @@ package com.ddang.ddang.chat.application; -import com.ddang.ddang.auction.domain.Auction; -import com.ddang.ddang.auction.domain.BidUnit; -import com.ddang.ddang.auction.domain.Price; -import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; -import com.ddang.ddang.category.domain.Category; -import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; -import com.ddang.ddang.chat.application.dto.CreateMessageDto; import com.ddang.ddang.chat.application.dto.ReadMessageDto; +import com.ddang.ddang.chat.application.event.MessageNotificationEvent; import com.ddang.ddang.chat.application.exception.ChatRoomNotFoundException; import com.ddang.ddang.chat.application.exception.MessageNotFoundException; -import com.ddang.ddang.chat.domain.ChatRoom; -import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; -import com.ddang.ddang.chat.infrastructure.persistence.JpaMessageRepository; -import com.ddang.ddang.chat.presentation.dto.request.ReadMessageRequest; +import com.ddang.ddang.chat.application.fixture.MessageServiceFixture; import com.ddang.ddang.configuration.IsolateDatabase; +import com.ddang.ddang.notification.application.NotificationService; +import com.ddang.ddang.notification.application.dto.CreateNotificationDto; +import com.ddang.ddang.notification.domain.NotificationStatus; import com.ddang.ddang.user.application.exception.UserNotFoundException; -import com.ddang.ddang.user.domain.User; -import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import com.google.firebase.messaging.FirebaseMessagingException; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.event.ApplicationEvents; +import org.springframework.test.context.event.RecordApplicationEvents; -import java.time.LocalDateTime; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; @IsolateDatabase +@RecordApplicationEvents @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class MessageServiceTest { +class MessageServiceTest extends MessageServiceFixture { @Autowired MessageService messageService; - @Autowired - JpaMessageRepository messageRepository; - - @Autowired - JpaAuctionRepository auctionRepository; + @MockBean + NotificationService notificationService; @Autowired - JpaUserRepository userRepository; - - @Autowired - JpaChatRoomRepository chatRoomRepository; - - @Autowired - JpaCategoryRepository categoryRepository; + ApplicationEvents events; @Test - void 메시지를_생성한다() { - // given - final BidUnit bidUnit = new BidUnit(1_000); - final Price startPrice = new Price(10_000); - final Category main = new Category("전자기기"); - final Category sub = new Category("노트북"); - - main.addSubCategory(sub); - - categoryRepository.save(main); - final Auction auction = Auction.builder() - .title("title") - .description("description") - .bidUnit(bidUnit) - .startPrice(startPrice) - .closingTime(LocalDateTime.now().plusDays(3L)) - .build(); - - auctionRepository.save(auction); - - final User writer = User.builder() - .name("발신자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(writer); - - final User receiver = User.builder() - .name("수신자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - - userRepository.save(receiver); - - final ChatRoom chatRoom = new ChatRoom(auction, writer); - - chatRoomRepository.save(chatRoom); - - final String contents = "메시지 내용"; - - final CreateMessageDto createMessageDto = new CreateMessageDto( - chatRoom.getId(), - writer.getId(), - receiver.getId(), - contents - ); + void 메시지를_생성한다() throws FirebaseMessagingException { + //given + given(notificationService.send(any(CreateNotificationDto.class))).willReturn(NotificationStatus.SUCCESS); // when - final Long messageId = messageService.create(createMessageDto); + final Long messageId = messageService.create(메시지_생성_DTO, 이미지_절대_경로); // then assertThat(messageId).isPositive(); } @Test - void 채팅방이_없는_경우_메시지를_생성하면_예외가_발생한다() { - // given - final BidUnit bidUnit = new BidUnit(1_000); - final Price startPrice = new Price(10_000); - final Category main = new Category("전자기기"); - final Category sub = new Category("노트북"); - - main.addSubCategory(sub); - - categoryRepository.save(main); - final Auction auction = Auction.builder() - .title("title") - .description("description") - .bidUnit(bidUnit) - .startPrice(startPrice) - .closingTime(LocalDateTime.now().plusDays(3L)) - .build(); - - auctionRepository.save(auction); - - final User writer = User.builder() - .name("발신자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(writer); + void 메시지를_생성하고_알림을_보낸다() { + // when + messageService.create(메시지_생성_DTO, 이미지_절대_경로); + final long actual = events.stream(MessageNotificationEvent.class).count(); - final User receiver = User.builder() - .name("수신자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); + // then + assertThat(actual).isEqualTo(1); + } - userRepository.save(receiver); + @Test + void 알림전송에_실패한_경우에도_정상적으로_메시지가_저장된다() throws FirebaseMessagingException { + // given + given(notificationService.send(any(CreateNotificationDto.class))).willReturn(NotificationStatus.FAIL); - final Long invalidChatRoomId = -999L; - final String contents = "메시지 내용"; + // when + final Long actual = messageService.create(메시지_생성_DTO, 이미지_절대_경로); - final CreateMessageDto createMessageDto = new CreateMessageDto( - invalidChatRoomId, - writer.getId(), - receiver.getId(), - contents - ); + // then + assertThat(actual).isPositive(); + } + @Test + void 채팅방이_없는_경우_메시지를_생성하면_예외가_발생한다() { // when & then - assertThatThrownBy(() -> messageService.create(createMessageDto)) + assertThatThrownBy(() -> messageService.create(유효하지_않은_채팅방의_메시지_생성_DTO, 이미지_절대_경로)) .isInstanceOf(ChatRoomNotFoundException.class) .hasMessageContaining("지정한 아이디에 대한 채팅방을 찾을 수 없습니다."); } @Test void 발신자가_없는_경우_메시지를_생성하면_예외가_발생한다() { - // given - final BidUnit bidUnit = new BidUnit(1_000); - final Price startPrice = new Price(10_000); - final Category main = new Category("전자기기"); - final Category sub = new Category("노트북"); - - main.addSubCategory(sub); - - categoryRepository.save(main); - - final Auction auction = Auction.builder() - .title("title") - .description("description") - .bidUnit(bidUnit) - .startPrice(startPrice) - .closingTime(LocalDateTime.now().plusDays(3L)) - .build(); - - auctionRepository.save(auction); - - final User receiver = User.builder() - .name("수신자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(receiver); - - - final ChatRoom chatRoom = new ChatRoom(auction, receiver); - - chatRoomRepository.save(chatRoom); - - final String contents = "메시지 내용"; - final Long invalidWriterId = -999L; - - final CreateMessageDto createMessageDto = new CreateMessageDto( - chatRoom.getId(), - invalidWriterId, - receiver.getId(), - contents - ); - - assertThatThrownBy(() -> messageService.create(createMessageDto)) + // when & then + assertThatThrownBy(() -> messageService.create(유효하지_않은_발신자의_메시지_생성_DTO, 이미지_절대_경로)) .isInstanceOf(UserNotFoundException.class) .hasMessageContaining("지정한 아이디에 대한 발신자를 찾을 수 없습니다."); } @Test void 수신자가_없는_경우_메시지를_생성하면_예외가_발생한다() { - // given - final BidUnit bidUnit = new BidUnit(1_000); - final Price startPrice = new Price(10_000); - final Category main = new Category("전자기기"); - final Category sub = new Category("노트북"); - - main.addSubCategory(sub); - - categoryRepository.save(main); - - final Auction auction = Auction.builder() - .title("title") - .description("description") - .bidUnit(bidUnit) - .startPrice(startPrice) - .closingTime(LocalDateTime.now().plusDays(3L)) - .build(); - - auctionRepository.save(auction); - - final User writer = User.builder() - .name("발신자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(writer); - - - final ChatRoom chatRoom = new ChatRoom(auction, writer); - - chatRoomRepository.save(chatRoom); - - final Long invalidReceiverId = -999L; - final String contents = "메시지 내용"; - - final CreateMessageDto createMessageDto = new CreateMessageDto( - chatRoom.getId(), - writer.getId(), - invalidReceiverId, - contents - ); - - assertThatThrownBy(() -> messageService.create(createMessageDto)) + // when & then + assertThatThrownBy(() -> messageService.create(유효하지_않은_수신자의_메시지_생성_DTO, 이미지_절대_경로)) .isInstanceOf(UserNotFoundException.class) .hasMessageContaining("지정한 아이디에 대한 수신자를 찾을 수 없습니다."); } - // TODO : [4차 데모 이후 리팩토링] BaseTimeEntity 시간 모킹하는 방법 찾아보고 수정 예정 - @Test void 마지막_조회_메시지가_없는_경우_모든_메시지를_조회한다() { - // given - final User writer = User.builder() - .name("발신자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("78923") - .build(); - - userRepository.save(writer); - - final User receiver = User.builder() - .name("수신자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(receiver); - - final Category main = new Category("전자기기"); - final Category sub = new Category("노트북"); - - main.addSubCategory(sub); - - categoryRepository.save(main); - - final Auction auction = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .seller(writer) - .subCategory(sub) - .build(); - - auctionRepository.save(auction); - - final ChatRoom chatRoom = new ChatRoom(auction, writer); - - chatRoomRepository.save(chatRoom); - - final String contents = "메시지 내용"; - - final CreateMessageDto createMessageDto = new CreateMessageDto( - chatRoom.getId(), - writer.getId(), - receiver.getId(), - contents - ); - - final int messagesCount = 10; - for (int count = 0; count < messagesCount; count++) { - messageService.create(createMessageDto); - } - - final Long lastMessageId = null; - final ReadMessageRequest request = new ReadMessageRequest(writer.getId(), chatRoom.getId(), lastMessageId); - // when - final List readMessageDtos = messageService.readAllByLastMessageId(request); + final List actual = messageService.readAllByLastMessageId(마지막_조회_메시지_아이디가_없는_메시지_조회용_request); // then - assertThat(readMessageDtos).hasSize(messagesCount); + assertThat(actual).hasSize(메시지_총_개수); } @Test void 첫_번째_메시지_이후에_생성된_모든_메시지를_조회한다() { - // given - final User writer = User.builder() - .name("발신자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(writer); - - final User receiver = User.builder() - .name("수신자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("56789") - .build(); - - userRepository.save(receiver); - - final BidUnit bidUnit = new BidUnit(1_000); - final Price startPrice = new Price(10_000); - final Category main = new Category("전자기기"); - final Category sub = new Category("노트북"); - - main.addSubCategory(sub); - - categoryRepository.save(main); - - main.addSubCategory(sub); - - categoryRepository.save(main); - - final Auction auction = Auction.builder() - .title("title") - .description("description") - .bidUnit(bidUnit) - .startPrice(startPrice) - .closingTime(LocalDateTime.now().plusDays(3L)) - .seller(writer) - .subCategory(sub) - .build(); - - auctionRepository.save(auction); - - final ChatRoom chatRoom = new ChatRoom(auction, writer); - - chatRoomRepository.save(chatRoom); - - final String contents = "메시지 내용"; - - final CreateMessageDto createMessageDto = new CreateMessageDto( - chatRoom.getId(), - writer.getId(), - receiver.getId(), - contents - ); - - final Long firstMessageId = messageService.create(createMessageDto); - - final int messagesCount = 10; - for (int count = 0; count < messagesCount; count++) { - messageService.create(createMessageDto); - } - - final ReadMessageRequest request = new ReadMessageRequest(writer.getId(), chatRoom.getId(), firstMessageId); - // when - final List readMessageDtos = messageService.readAllByLastMessageId(request); + final List actual = messageService.readAllByLastMessageId(두_번째_메시지부터_모든_메시지_조회용_request); // then - assertThat(readMessageDtos).hasSize(messagesCount); + assertThat(actual).hasSize(메시지_총_개수 - 1); } @Test void 마지막으로_조회된_메시지_이후에_추가된_메시지가_없는_경우_빈_리스트를_반환한다() { - // given - final Category main = new Category("전자기기"); - final Category sub = new Category("노트북"); - - main.addSubCategory(sub); - - categoryRepository.save(main); - - final User writer = User.builder() - .name("발신자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("78923") - .build(); - - userRepository.save(writer); - - final User receiver = User.builder() - .name("수신자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(receiver); - - - final Auction auction = Auction.builder() - .seller(writer) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - - auctionRepository.save(auction); - - final ChatRoom chatRoom = new ChatRoom(auction, writer); - - chatRoomRepository.save(chatRoom); - - final String contents = "메시지 내용"; - - final CreateMessageDto createMessageDto = new CreateMessageDto( - chatRoom.getId(), - writer.getId(), - receiver.getId(), - contents - ); - - final int messagesCount = 10; - for (int count = 0; count < messagesCount; count++) { - messageService.create(createMessageDto); - } - - final Long lastMessageId = messageService.create(createMessageDto); - - final ReadMessageRequest request = new ReadMessageRequest(writer.getId(), chatRoom.getId(), lastMessageId); - // when - final List readMessageDtos = messageService.readAllByLastMessageId(request); + final List readMessageDtos = messageService.readAllByLastMessageId(조회할_메시지가_더이상_없는_메시지_조회용_request); // then - assertThat(readMessageDtos).hasSize(0); + assertThat(readMessageDtos).isEmpty(); } @Test void 잘못된_사용자가_메시지를_조회할_경우_예외가_발생한다() { - // given - final BidUnit bidUnit = new BidUnit(1_000); - final Price startPrice = new Price(10_000); - final Category main = new Category("전자기기"); - final Category sub = new Category("노트북"); - - main.addSubCategory(sub); - - categoryRepository.save(main); - final Auction auction = Auction.builder() - .title("title") - .description("description") - .bidUnit(bidUnit) - .startPrice(startPrice) - .closingTime(LocalDateTime.now().plusDays(3L)) - .build(); - - auctionRepository.save(auction); - - final User writer = User.builder() - .name("발신자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("78923") - .build(); - - userRepository.save(writer); - - final User receiver = User.builder() - .name("수신자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(receiver); - - final ChatRoom chatRoom = new ChatRoom(auction, writer); - - chatRoomRepository.save(chatRoom); - - final String contents = "메시지 내용"; - - final CreateMessageDto createMessageDto = new CreateMessageDto( - chatRoom.getId(), - writer.getId(), - receiver.getId(), - contents - ); - - final Long lastMessageId = messageService.create(createMessageDto); - - final Long invalidUserId = -999L; - final ReadMessageRequest request = new ReadMessageRequest(invalidUserId, chatRoom.getId(), lastMessageId); - // when & then - assertThatThrownBy(() -> messageService.readAllByLastMessageId(request)) + assertThatThrownBy(() -> messageService.readAllByLastMessageId(유효하지_않은_사용자의_메시지_조회용_request)) .isInstanceOf(UserNotFoundException.class) .hasMessageContaining("지정한 아이디에 대한 사용자를 찾을 수 없습니다."); } @Test void 조회한_채팅방이_없는_경우_예외가_발생한다() { - // given - final BidUnit bidUnit = new BidUnit(1_000); - final Price startPrice = new Price(10_000); - final Category main = new Category("전자기기"); - final Category sub = new Category("노트북"); - - main.addSubCategory(sub); - - categoryRepository.save(main); - final Auction auction = Auction.builder() - .title("title") - .description("description") - .bidUnit(bidUnit) - .startPrice(startPrice) - .closingTime(LocalDateTime.now().plusDays(3L)) - .build(); - - auctionRepository.save(auction); - - final User writer = User.builder() - .name("발신자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("78923") - .build(); - - userRepository.save(writer); - - final User receiver = User.builder() - .name("수신자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(receiver); - - final ChatRoom chatRoom = new ChatRoom(auction, writer); - - chatRoomRepository.save(chatRoom); - - final Long invalidChatRoomId = -999L; - final String contents = "메시지 내용"; - - final CreateMessageDto createMessageDto = new CreateMessageDto( - chatRoom.getId(), - writer.getId(), - receiver.getId(), - contents - ); - - final Long messageId = messageService.create(createMessageDto); - - final ReadMessageRequest request = new ReadMessageRequest(writer.getId(), invalidChatRoomId, messageId); - // when & then - assertThatThrownBy(() -> messageService.readAllByLastMessageId(request)) + assertThatThrownBy(() -> messageService.readAllByLastMessageId(유효하지_않은_채팅방의_메시지_조회용_request)) .isInstanceOf(ChatRoomNotFoundException.class) .hasMessageContaining("지정한 아이디에 대한 채팅방을 찾을 수 없습니다."); } @Test void 조회한_마지막_메시지가_없는_경우_예외가_발생한다() { - // given - final BidUnit bidUnit = new BidUnit(1_000); - final Price startPrice = new Price(10_000); - final Category main = new Category("전자기기"); - final Category sub = new Category("노트북"); - - main.addSubCategory(sub); - - categoryRepository.save(main); - final Auction auction = Auction.builder() - .title("title") - .description("description") - .bidUnit(bidUnit) - .startPrice(startPrice) - .closingTime(LocalDateTime.now().plusDays(3L)) - .build(); - - auctionRepository.save(auction); - - final User writer = User.builder() - .name("발신자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("78923") - .build(); - - userRepository.save(writer); - - final User receiver = User.builder() - .name("수신자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(receiver); - - final ChatRoom chatRoom = new ChatRoom(auction, writer); - - chatRoomRepository.save(chatRoom); - - final Long invalidLastMessageId = -999L; - - final ReadMessageRequest request = new ReadMessageRequest(writer.getId(), chatRoom.getId(), invalidLastMessageId); - // when & then - assertThatThrownBy(() -> messageService.readAllByLastMessageId(request)) + assertThatThrownBy(() -> messageService.readAllByLastMessageId(존재하지_않는_마지막_메시지_아이디의_메시지_조회용_request)) .isInstanceOf(MessageNotFoundException.class) .hasMessageContaining("조회한 마지막 메시지가 존재하지 않습니다."); } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/ChatRoomServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/ChatRoomServiceFixture.java new file mode 100644 index 000000000..9d3adcd1d --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/ChatRoomServiceFixture.java @@ -0,0 +1,251 @@ +package com.ddang.ddang.chat.application.fixture; + +import com.ddang.ddang.auction.application.dto.ReadChatRoomDto; +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.authentication.domain.dto.AuthenticationUserInfo; +import com.ddang.ddang.bid.domain.Bid; +import com.ddang.ddang.bid.domain.BidPrice; +import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.chat.application.dto.CreateChatRoomDto; +import com.ddang.ddang.chat.application.dto.ReadAuctionInChatRoomDto; +import com.ddang.ddang.chat.application.dto.ReadChatRoomWithLastMessageDto; +import com.ddang.ddang.chat.application.dto.ReadLastMessageDto; +import com.ddang.ddang.chat.application.dto.ReadParticipatingChatRoomDto; +import com.ddang.ddang.chat.application.dto.ReadUserInChatRoomDto; +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; +import com.ddang.ddang.chat.infrastructure.persistence.JpaMessageRepository; +import com.ddang.ddang.chat.infrastructure.persistence.dto.ChatRoomAndImageDto; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class ChatRoomServiceFixture { + + @Autowired + private JpaCategoryRepository categoryRepository; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaBidRepository bidRepository; + + @Autowired + private JpaChatRoomRepository chatRoomRepository; + + @Autowired + private JpaMessageRepository messageRepository; + + protected User 판매자; + protected User 구매자; + protected User 엔초; + protected User 제이미; + protected User 지토; + protected User 경매에_참여한_적_없는_사용자; + protected Auction 채팅방이_없는_경매; + protected Auction 판매자_엔초_구매자_지토_경매; + protected ChatRoom 엔초_지토_채팅방; + private ChatRoom 제이미_엔초_채팅방; + private Message 엔초가_지토에게_1시에_보낸_쪽지; + private Message 제이미가_엔초에게_2시에_보낸_쪽지; + protected Long 존재하지_않는_사용자_아이디 = -999L; + protected Long 존재하지_않는_경매_아이디 = -999L; + protected Long 존재하지_않는_채팅방_아이디 = -999L; + protected AuthenticationUserInfo 엔초_회원_정보; + protected AuthenticationUserInfo 판매자_회원_정보; + protected AuthenticationUserInfo 경매에_참여한_적_없는_사용자_정보; + protected AuthenticationUserInfo 존재하지_않는_사용자_정보; + protected CreateChatRoomDto 채팅방_생성을_위한_DTO; + protected CreateChatRoomDto 경매_정보가_없어서_채팅방을_생성할_수_없는_DTO; + protected CreateChatRoomDto 경매가_진행중이라서_채팅방을_생성할_수_없는_DTO; + protected CreateChatRoomDto 낙찰자가_없어서_채팅방을_생성할_수_없는_DTO; + protected CreateChatRoomDto 엔초_지토_채팅방_생성을_위한_DTO; + protected ReadParticipatingChatRoomDto 엔초가_조회한_엔초_지토_채팅방_정보_조회_결과; + protected ReadChatRoomDto 엔초_지토_채팅방_정보_및_참여_가능; + protected ReadChatRoomDto 엔초_지토_채팅방_정보_및_참여_불가능; + protected ReadChatRoomDto 채팅방은_아직_없지만_참여_가능; + protected ReadChatRoomWithLastMessageDto 엔초_채팅_목록의_제이미_엔초_채팅방_정보; + protected ReadChatRoomWithLastMessageDto 엔초_채팅_목록의_엔초_지토_채팅방_정보; + + @BeforeEach + void setUp() { + final Category 전자기기_카테고리 = new Category("전자기기"); + final Category 전자기기_서브_노트북_카테고리 = new Category("노트북 카테고리"); + 전자기기_카테고리.addSubCategory(전자기기_서브_노트북_카테고리); + categoryRepository.save(전자기기_카테고리); + + final ProfileImage 프로필_이미지 = new ProfileImage("upload.png", "store.png"); + final AuctionImage 경매_대표_이미지 = new AuctionImage("경매_대표_이미지.png", "경매_대표_이미지.png"); + final AuctionImage 대표_이미지가_아닌_경매_이미지 = + new AuctionImage("대표 이미지가_아닌_경매_이미지.png", "대표 이미지가_아닌_경매_이미지.png"); + final AuctionImage 엔초의_경매_대표_이미지 = new AuctionImage("엔초의_경매_대표_이미지.png", "엔초의_경매_대표_이미지.png"); + final AuctionImage 엔초의_대표_이미지가_아닌_경매_이미지 = + new AuctionImage("엔초의_대표 이미지가_아닌_경매_이미지.png", "엔초의_대표 이미지가_아닌_경매_이미지.png"); + final AuctionImage 제이미의_경매_대표_이미지 = new AuctionImage("제이미의_경매_대표_이미지.png", "제이미의_경매_대표_이미지.png"); + final AuctionImage 제이미의_대표_이미지가_아닌_경매_이미지 = + new AuctionImage("제이미의_대표 이미지가_아닌_경매_이미지.png", "제이미의_대표 이미지가_아닌_경매_이미지.png"); + + 판매자 = User.builder() + .name("판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + 구매자 = User.builder() + .name("구매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + 엔초 = User.builder() + .name("엔초") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + 제이미 = User.builder() + .name("제이미") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + 지토 = User.builder() + .name("지토") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12348") + .build(); + 경매에_참여한_적_없는_사용자 = User.builder() + .name("외부인") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12349") + .build(); + userRepository.saveAll(List.of(판매자, 구매자, 엔초, 제이미, 지토, 경매에_참여한_적_없는_사용자)); + + 채팅방이_없는_경매 = Auction.builder() + .seller(판매자) + .title("맥북") + .description("맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + final Auction 종료되지_않은_경매 = Auction.builder() + .seller(판매자) + .title("맥북") + .description("맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now().plusDays(10L)) + .build(); + final Auction 낙찰자가_없는_경매 = Auction.builder() + .seller(판매자) + .title("맥북") + .description("맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 판매자_엔초_구매자_지토_경매 = Auction.builder() + .seller(엔초) + .title("엔초 맥북") + .description("엔초 맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + final Auction 판매자_제이미_구매자_엔초_경매 = Auction.builder() + .seller(제이미) + .title("제이미 맥북") + .description("제이미 맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 채팅방이_없는_경매.addAuctionImages(List.of(경매_대표_이미지, 대표_이미지가_아닌_경매_이미지)); + 판매자_엔초_구매자_지토_경매.addAuctionImages(List.of(엔초의_경매_대표_이미지, 엔초의_대표_이미지가_아닌_경매_이미지)); + 판매자_제이미_구매자_엔초_경매.addAuctionImages(List.of(제이미의_경매_대표_이미지, 제이미의_대표_이미지가_아닌_경매_이미지)); + auctionRepository.saveAll( + List.of(채팅방이_없는_경매, 종료되지_않은_경매, 낙찰자가_없는_경매, 판매자_엔초_구매자_지토_경매, 판매자_제이미_구매자_엔초_경매) + ); + + final Bid 채팅방_없는_경매_입찰 = new Bid(채팅방이_없는_경매, 구매자, new BidPrice(15_000)); + final Bid 지토가_엔초_경매에_입찰 = new Bid(판매자_엔초_구매자_지토_경매, 지토, new BidPrice(15_000)); + final Bid 엔초가_제이미_경매에_입찰 = new Bid(판매자_제이미_구매자_엔초_경매, 엔초, new BidPrice(15_000)); + bidRepository.saveAll(List.of(채팅방_없는_경매_입찰, 지토가_엔초_경매에_입찰, 엔초가_제이미_경매에_입찰)); + 채팅방이_없는_경매.updateLastBid(채팅방_없는_경매_입찰); + 판매자_엔초_구매자_지토_경매.updateLastBid(지토가_엔초_경매에_입찰); + 판매자_제이미_구매자_엔초_경매.updateLastBid(엔초가_제이미_경매에_입찰); + + 엔초_지토_채팅방 = new ChatRoom(판매자_엔초_구매자_지토_경매, 지토); + 제이미_엔초_채팅방 = new ChatRoom(판매자_제이미_구매자_엔초_경매, 엔초); + chatRoomRepository.saveAll(List.of(엔초_지토_채팅방, 제이미_엔초_채팅방)); + + 엔초가_지토에게_1시에_보낸_쪽지 = Message.builder() + .chatRoom(엔초_지토_채팅방) + .contents("엔초가 지토에게 1시애 보낸 쪽지") + .writer(엔초) + .receiver(지토) + .build(); + 제이미가_엔초에게_2시에_보낸_쪽지 = Message.builder() + .chatRoom(제이미_엔초_채팅방) + .contents("제이미가 엔초에게 2시애 보낸 쪽지") + .writer(제이미) + .receiver(엔초) + .build(); + messageRepository.saveAll(List.of(엔초가_지토에게_1시에_보낸_쪽지, 제이미가_엔초에게_2시에_보낸_쪽지)); + + final ChatRoomAndImageDto 엔초_지토_채팅방_정보 = new ChatRoomAndImageDto(엔초_지토_채팅방, 엔초의_경매_대표_이미지); + 엔초_회원_정보 = new AuthenticationUserInfo(엔초.getId()); + 판매자_회원_정보 = new AuthenticationUserInfo(판매자.getId()); + 경매에_참여한_적_없는_사용자_정보 = new AuthenticationUserInfo(경매에_참여한_적_없는_사용자.getId()); + 존재하지_않는_사용자_정보 = new AuthenticationUserInfo(존재하지_않는_사용자_아이디); + 채팅방_생성을_위한_DTO = new CreateChatRoomDto(채팅방이_없는_경매.getId()); + 경매_정보가_없어서_채팅방을_생성할_수_없는_DTO = new CreateChatRoomDto(존재하지_않는_경매_아이디); + 경매가_진행중이라서_채팅방을_생성할_수_없는_DTO = new CreateChatRoomDto(종료되지_않은_경매.getId()); + 낙찰자가_없어서_채팅방을_생성할_수_없는_DTO = new CreateChatRoomDto(낙찰자가_없는_경매.getId()); + 엔초_지토_채팅방_생성을_위한_DTO = new CreateChatRoomDto(판매자_엔초_구매자_지토_경매.getId()); + 엔초가_조회한_엔초_지토_채팅방_정보_조회_결과 = ReadParticipatingChatRoomDto.of(엔초, 엔초_지토_채팅방_정보); + 엔초_지토_채팅방_정보_및_참여_가능 = new ReadChatRoomDto(엔초_지토_채팅방.getId(), true); + 엔초_지토_채팅방_정보_및_참여_불가능 = new ReadChatRoomDto(엔초_지토_채팅방.getId(), false); + 채팅방은_아직_없지만_참여_가능 = new ReadChatRoomDto(null, true); + 엔초_채팅_목록의_제이미_엔초_채팅방_정보 = new ReadChatRoomWithLastMessageDto( + 제이미_엔초_채팅방.getId(), + ReadAuctionInChatRoomDto.of(판매자_제이미_구매자_엔초_경매, 제이미의_경매_대표_이미지), + ReadUserInChatRoomDto.from(제이미), + ReadLastMessageDto.from(제이미가_엔초에게_2시에_보낸_쪽지), + true + ); + 엔초_채팅_목록의_엔초_지토_채팅방_정보 = new ReadChatRoomWithLastMessageDto( + 엔초_지토_채팅방.getId(), + ReadAuctionInChatRoomDto.of(판매자_엔초_구매자_지토_경매, 엔초의_경매_대표_이미지), + ReadUserInChatRoomDto.from(지토), + ReadLastMessageDto.from(엔초가_지토에게_1시에_보낸_쪽지), + true + ); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/MessageServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/MessageServiceFixture.java new file mode 100644 index 000000000..05e417e85 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/MessageServiceFixture.java @@ -0,0 +1,151 @@ +package com.ddang.ddang.chat.application.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.chat.application.dto.CreateMessageDto; +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; +import com.ddang.ddang.chat.infrastructure.persistence.JpaMessageRepository; +import com.ddang.ddang.chat.presentation.dto.request.ReadMessageRequest; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class MessageServiceFixture { + + @Autowired + private JpaMessageRepository messageRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaChatRoomRepository chatRoomRepository; + + @Autowired + private JpaCategoryRepository categoryRepository; + + protected CreateMessageDto 메시지_생성_DTO; + protected CreateMessageDto 유효하지_않은_채팅방의_메시지_생성_DTO; + protected CreateMessageDto 유효하지_않은_발신자의_메시지_생성_DTO; + protected CreateMessageDto 유효하지_않은_수신자의_메시지_생성_DTO; + protected CreateMessageDto 수신자가_탈퇴한_경우_메시지_생성_DTO; + protected ReadMessageRequest 마지막_조회_메시지_아이디가_없는_메시지_조회용_request; + protected ReadMessageRequest 두_번째_메시지부터_모든_메시지_조회용_request; + protected ReadMessageRequest 조회할_메시지가_더이상_없는_메시지_조회용_request; + protected ReadMessageRequest 유효하지_않은_사용자의_메시지_조회용_request; + protected ReadMessageRequest 유효하지_않은_채팅방의_메시지_조회용_request; + protected ReadMessageRequest 존재하지_않는_마지막_메시지_아이디의_메시지_조회용_request; + + protected String 이미지_절대_경로 = "/imageUrl"; + protected int 메시지_총_개수 = 10; + + @BeforeEach + void setUp() { + final Category 전자기기 = new Category("전자기기"); + final Category 전자기기_하위_노트북 = new Category("노트북"); + 전자기기.addSubCategory(전자기기_하위_노트북); + categoryRepository.save(전자기기); + + final Auction 경매 = Auction.builder() + .title("경매") + .description("description") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(10_000)) + .closingTime(LocalDateTime.now().plusDays(3L)) + .build(); + auctionRepository.save(경매); + + final User 발신자 = User.builder() + .name("발신자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + final User 수신자 = User.builder() + .name("수신자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + final User 탈퇴한_사용자 = User.builder() + .name("탈퇴한 사용자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + 탈퇴한_사용자.withdrawal(); + userRepository.saveAll(List.of(발신자, 수신자, 탈퇴한_사용자)); + + final ChatRoom 채팅방 = new ChatRoom(경매, 발신자); + final ChatRoom 탈퇴한_사용자와의_채팅방 = new ChatRoom(경매, 탈퇴한_사용자); + chatRoomRepository.saveAll(List.of(채팅방, 탈퇴한_사용자와의_채팅방)); + + 메시지_생성_DTO = new CreateMessageDto( + 채팅방.getId(), + 발신자.getId(), + 수신자.getId(), + "메시지 내용" + ); + 유효하지_않은_채팅방의_메시지_생성_DTO = new CreateMessageDto( + -999L, + 발신자.getId(), + 수신자.getId(), + "메시지 내용" + ); + 유효하지_않은_발신자의_메시지_생성_DTO = new CreateMessageDto( + 채팅방.getId(), + -999L, + 수신자.getId(), + "메시지 내용" + ); + 유효하지_않은_수신자의_메시지_생성_DTO = new CreateMessageDto( + 채팅방.getId(), + 발신자.getId(), + -999L, + "메시지 내용" + ); + 수신자가_탈퇴한_경우_메시지_생성_DTO = new CreateMessageDto( + 탈퇴한_사용자와의_채팅방.getId(), + 발신자.getId(), + 탈퇴한_사용자.getId(), + "메시지 내용" + ); + + final List 메시지들 = new ArrayList<>(); + for (int count = 0; count < 메시지_총_개수; count++) { + final Message 메시지 = Message.builder() + .writer(발신자) + .receiver(수신자) + .chatRoom(채팅방) + .contents("메시지 내용") + .build(); + 메시지들.add(메시지); + messageRepository.save(메시지); + } + + 마지막_조회_메시지_아이디가_없는_메시지_조회용_request = new ReadMessageRequest(발신자.getId(), 채팅방.getId(), null); + 두_번째_메시지부터_모든_메시지_조회용_request = new ReadMessageRequest(발신자.getId(), 채팅방.getId(), 메시지들.get(0).getId()); + 조회할_메시지가_더이상_없는_메시지_조회용_request = new ReadMessageRequest(발신자.getId(), 채팅방.getId(), 메시지들.get(메시지_총_개수 - 1) + .getId()); + 유효하지_않은_사용자의_메시지_조회용_request = new ReadMessageRequest(-999L, 채팅방.getId(), null); + 유효하지_않은_채팅방의_메시지_조회용_request = new ReadMessageRequest(발신자.getId(), -999L, null); + 존재하지_않는_마지막_메시지_아이디의_메시지_조회용_request = new ReadMessageRequest(발신자.getId(), 채팅방.getId(), -999L); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/domain/ChatRoomTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/domain/ChatRoomTest.java index 57dc00e75..a78f12fd5 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/domain/ChatRoomTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/domain/ChatRoomTest.java @@ -1,153 +1,60 @@ package com.ddang.ddang.chat.domain; -import com.ddang.ddang.auction.domain.Auction; -import com.ddang.ddang.auction.domain.BidUnit; -import com.ddang.ddang.auction.domain.Price; -import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; -import com.ddang.ddang.bid.domain.Bid; -import com.ddang.ddang.bid.domain.BidPrice; -import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository; -import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; -import com.ddang.ddang.configuration.JpaConfiguration; -import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.ddang.ddang.chat.domain.fixture.ChatRoomFixture; import com.ddang.ddang.user.domain.User; -import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; - -import java.time.LocalDateTime; import static org.assertj.core.api.Assertions.assertThat; -@DataJpaTest @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -@Import({JpaConfiguration.class, QuerydslConfiguration.class}) -class ChatRoomTest { - - @PersistenceContext - EntityManager em; +class ChatRoomTest extends ChatRoomFixture { - @Autowired - JpaAuctionRepository auctionRepository; + @Test + void 탈퇴하지_않은_사용자와는_채팅이_가능하다() { - @Autowired - JpaBidRepository bidRepository; + final ChatRoom chatRoom = new ChatRoom(경매, 구매자); - @Autowired - JpaUserRepository userRepository; + // when + final boolean actual = chatRoom.isChatAvailablePartner(구매자); - @Autowired - JpaChatRoomRepository chatRoomRepository; + // then + assertThat(actual).isTrue(); + } - @ParameterizedTest - @CsvSource(value = {"0:true", "9:true", "10:false"}, delimiter = ':') - void 채팅방_비활성화_여부를_체크한다(final long plusDay, final boolean expected) { + @Test + void 탈퇴한_사용자와는_채팅이_불가능하다() { // given - final User buyer = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .title("title") - .build(); - - userRepository.save(buyer); - auctionRepository.save(auction); - - final ChatRoom chatRoom = new ChatRoom(auction, buyer); - chatRoomRepository.save(chatRoom); - - em.flush(); - em.clear(); + final ChatRoom chatRoom = new ChatRoom(경매, 탈퇴한_사용자); // when - final boolean actual = chatRoom.isChatAvailableTime(chatRoom.getCreatedTime().plusDays(plusDay)); + final boolean actual = chatRoom.isChatAvailablePartner(탈퇴한_사용자); // then - assertThat(actual).isEqualTo(expected); + assertThat(actual).isFalse(); } @Test void 주어진_사용자의_채팅상대를_반환한다() { // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User buyer = User.builder() - .name("구매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - userRepository.save(seller); - userRepository.save(buyer); - - final Auction auction = Auction.builder() - .title("title") - .seller(seller) - .build(); - - auctionRepository.save(auction); - - final ChatRoom chatRoom = new ChatRoom(auction, buyer); - chatRoomRepository.save(chatRoom); - - em.flush(); - em.clear(); + final ChatRoom chatRoom = new ChatRoom(경매, 구매자); // when - final User actual = chatRoom.calculateChatPartnerOf(buyer); + final User actual = chatRoom.calculateChatPartnerOf(구매자); // then - assertThat(actual).isEqualTo(seller); + assertThat(actual).isEqualTo(판매자); } @Test void 주어진_사용자가_판매자라면_채팅_참여자이다() { // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User buyer = User.builder() - .name("구매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - userRepository.save(seller); - userRepository.save(buyer); - - final Auction auction = Auction.builder() - .title("경매") - .seller(seller) - .build(); - auctionRepository.save(auction); - - final ChatRoom chatRoom = new ChatRoom(auction, buyer); - chatRoomRepository.save(chatRoom); - - em.flush(); - em.clear(); + final ChatRoom chatRoom = new ChatRoom(경매, 구매자); // when - final boolean actual = chatRoom.isParticipant(seller); + final boolean actual = chatRoom.isParticipant(판매자); // then assertThat(actual).isTrue(); @@ -156,42 +63,10 @@ class ChatRoomTest { @Test void 주어진_사용자가_구매자라면_채팅_참여자이다() { // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User buyer = User.builder() - .name("구매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - userRepository.save(seller); - userRepository.save(buyer); - - final Auction auction = Auction.builder() - .title("경매") - .seller(seller) - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(10_000)) - .closingTime(LocalDateTime.now()) - .build(); - auctionRepository.save(auction); - - final Bid bid = new Bid(auction, buyer, new BidPrice(15_000)); - bidRepository.save(bid); - auction.updateLastBid(bid); - - final ChatRoom chatRoom = new ChatRoom(auction, buyer); - chatRoomRepository.save(chatRoom); - - em.flush(); - em.clear(); + final ChatRoom chatRoom = new ChatRoom(경매, 구매자); // when - final boolean actual = chatRoom.isParticipant(buyer); + final boolean actual = chatRoom.isParticipant(구매자); // then assertThat(actual).isTrue(); @@ -200,49 +75,10 @@ class ChatRoomTest { @Test void 주어진_사용자가_판매자와_구매자_모두_아니라면_채팅_참여자가_아니다() { // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User buyer = User.builder() - .name("구매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - final User stranger = User.builder() - .name("일반인") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12347") - .build(); - userRepository.save(seller); - userRepository.save(buyer); - userRepository.save(stranger); - - final Auction auction = Auction.builder() - .title("경매") - .seller(seller) - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(10_000)) - .closingTime(LocalDateTime.now()) - .build(); - auctionRepository.save(auction); - - final Bid bid = new Bid(auction, buyer, new BidPrice(15_000)); - bidRepository.save(bid); - auction.updateLastBid(bid); - - final ChatRoom chatRoom = new ChatRoom(auction, buyer); - chatRoomRepository.save(chatRoom); - - em.flush(); - em.clear(); + final ChatRoom chatRoom = new ChatRoom(경매, 구매자); // when - final boolean actual = chatRoom.isParticipant(stranger); + final boolean actual = chatRoom.isParticipant(경매에_참여하지_않는_사용자); // then assertThat(actual).isFalse(); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/domain/fixture/ChatRoomFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/domain/fixture/ChatRoomFixture.java new file mode 100644 index 000000000..ef6c057e8 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/domain/fixture/ChatRoomFixture.java @@ -0,0 +1,72 @@ +package com.ddang.ddang.chat.domain.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.bid.domain.Bid; +import com.ddang.ddang.bid.domain.BidPrice; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDateTime; + +@SuppressWarnings("NonAsciiCharacters") +public class ChatRoomFixture { + + protected User 판매자; + protected User 구매자; + protected User 경매에_참여하지_않는_사용자; + protected User 탈퇴한_사용자; + protected Auction 경매; + protected Bid 입찰; + + @BeforeEach + void setUp() { + 판매자 = User.builder() + .name("판매자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + 구매자 = User.builder() + .name("구매자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + 경매에_참여하지_않는_사용자 = User.builder() + .name("경매에 참여하지 않는 사용자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + 탈퇴한_사용자 = User.builder() + .name("탈퇴한 사용자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12348") + .build(); + 탈퇴한_사용자.withdrawal(); + + 경매 = Auction.builder() + .seller(판매자) + .title("맥북") + .description("맥북 팔아요") + .subCategory(new Category("전자기기")) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 입찰 = new Bid(경매, 구매자, new BidPrice(15_000)); + + ReflectionTestUtils.setField(판매자, "id", 1L); + ReflectionTestUtils.setField(구매자, "id", 2L); + ReflectionTestUtils.setField(경매에_참여하지_않는_사용자, "id", 3L); + + 경매.updateLastBid(입찰); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/JpaChatRoomRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/JpaChatRoomRepositoryTest.java index 104bb143d..074ef8d34 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/JpaChatRoomRepositoryTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/JpaChatRoomRepositoryTest.java @@ -1,20 +1,11 @@ package com.ddang.ddang.chat.infrastructure.persistence; -import com.ddang.ddang.auction.domain.Auction; -import com.ddang.ddang.auction.domain.BidUnit; -import com.ddang.ddang.auction.domain.Price; -import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; -import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository; -import com.ddang.ddang.category.domain.Category; -import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.infrastructure.persistence.fixture.JpaChatRoomRepositoryFixture; import com.ddang.ddang.configuration.JpaConfiguration; import com.ddang.ddang.configuration.QuerydslConfiguration; -import com.ddang.ddang.user.domain.User; -import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; -import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -22,52 +13,26 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -import java.time.LocalDateTime; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -@Import({JpaConfiguration.class, QuerydslConfiguration.class}) -class JpaChatRoomRepositoryTest { +class JpaChatRoomRepositoryTest extends JpaChatRoomRepositoryFixture { @PersistenceContext EntityManager em; - @Autowired - JpaAuctionRepository auctionRepository; - - @Autowired - JpaUserRepository userRepository; - - @Autowired - JpaCategoryRepository categoryRepository; - @Autowired JpaChatRoomRepository chatRoomRepository; - @Autowired - JpaBidRepository bidRepository; - @Test void 채팅방을_저장한다() { // given - final User buyer = User.builder() - .name("구매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .title("title") - .build(); - - userRepository.save(buyer); - auctionRepository.save(auction); - - final ChatRoom chatRoom = new ChatRoom(auction, buyer); + final ChatRoom chatRoom = new ChatRoom(경매, 구매자); // when chatRoomRepository.save(chatRoom); @@ -81,95 +46,26 @@ class JpaChatRoomRepositoryTest { @Test void 지정한_아이디에_대한_채팅방을_조회한다() { - // given - final User buyer = User.builder() - .name("구매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .title("title") - .build(); - - userRepository.save(buyer); - auctionRepository.save(auction); - - final ChatRoom expected = new ChatRoom(auction, buyer); - - chatRoomRepository.save(expected); - - em.flush(); - em.clear(); - // when - final Optional actual = chatRoomRepository.findById(expected.getId()); + final Optional actual = chatRoomRepository.findById(채팅방.getId()); // then - SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual).isPresent(); - softAssertions.assertThat(actual.get().getId()).isEqualTo(expected.getId()); - softAssertions.assertThat(actual.get().getAuction()).isEqualTo(expected.getAuction()); - softAssertions.assertThat(actual.get().getBuyer()).isEqualTo(expected.getBuyer()); - }); + assertThat(actual).contains(채팅방); } @Test void 지정한_경매_아이디가_포함된_채팅방이_존재한다면_참을_반환한다() { - // given - final Category main = new Category("메인"); - final Category sub = new Category("서브"); - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User buyer = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - userRepository.save(seller); - userRepository.save(buyer); - - final Auction auction = Auction.builder() - .title("경매 1") - .seller(seller) - .subCategory(sub) - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(10_000)) - .closingTime(LocalDateTime.now().minusDays(3L)) - .build(); - auctionRepository.save(auction); - - final ChatRoom chatRoom = new ChatRoom(auction, buyer); - chatRoomRepository.save(chatRoom); - - em.flush(); - em.clear(); - // when - final boolean actual = chatRoomRepository.existsByAuctionId(auction.getId()); + final boolean actual = chatRoomRepository.existsByAuctionId(경매.getId()); // then assertThat(actual).isTrue(); } @Test - void 지정한_경매_아이디가_포함된_채팅방이_존재한다면_거짓을_반환한다() { - // given - final Long invalidAuctionId = -999L; - - em.flush(); - em.clear(); - + void 지정한_경매_아이디가_포함된_채팅방이_존재하지_않는다면_거짓을_반환한다() { // when - final boolean actual = chatRoomRepository.existsByAuctionId(invalidAuctionId); + final boolean actual = chatRoomRepository.existsByAuctionId(존재하지_않는_채팅방_아이디); // then assertThat(actual).isFalse(); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/JpaMessageRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/JpaMessageRepositoryTest.java index 0fb75e2a4..c6e8cb278 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/JpaMessageRepositoryTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/JpaMessageRepositoryTest.java @@ -1,15 +1,9 @@ package com.ddang.ddang.chat.infrastructure.persistence; -import static org.assertj.core.api.Assertions.assertThat; - -import com.ddang.ddang.auction.domain.Auction; -import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; -import com.ddang.ddang.chat.domain.ChatRoom; import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.chat.infrastructure.persistence.fixture.JpaMessageRepositoryFixture; import com.ddang.ddang.configuration.JpaConfiguration; import com.ddang.ddang.configuration.QuerydslConfiguration; -import com.ddang.ddang.user.domain.User; -import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import org.junit.jupiter.api.DisplayNameGeneration; @@ -19,67 +13,35 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; +import static org.assertj.core.api.Assertions.assertThat; + @DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -@Import({JpaConfiguration.class, QuerydslConfiguration.class}) -class JpaMessageRepositoryTest { +class JpaMessageRepositoryTest extends JpaMessageRepositoryFixture { @PersistenceContext EntityManager em; - @Autowired - JpaAuctionRepository auctionRepository; - - @Autowired - JpaUserRepository userRepository; - - @Autowired - JpaChatRoomRepository chatRoomRepository; - @Autowired JpaMessageRepository messageRepository; @Test void 메시지를_저장한다() { - // given - final User participant1 = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User participant2 = User.builder() - .name("구매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - final Auction auction = Auction.builder() - .title("title") - .build(); - - userRepository.save(participant1); - userRepository.save(participant2); - auctionRepository.save(auction); - - final ChatRoom chatRoom = new ChatRoom(auction, participant2); - chatRoomRepository.save(chatRoom); - - final Message message = Message.builder() - .chatRoom(chatRoom) - .writer(participant1) - .receiver(participant2) - .contents("안녕하세요") - .build(); - // when - messageRepository.save(message); + final Message actual = messageRepository.save(메시지); - // then em.flush(); em.clear(); - assertThat(message.getId()).isPositive(); + // then + assertThat(actual.getId()).isPositive(); + } + + @Test + void 조회하려는_메시지_아이디가_존재하지_않는_경우_거짓을_반환한다() { + // when & then + assertThat(messageRepository.existsById(유효하지_않은_메시지_아이디)).isFalse(); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndImageRepositoryImplTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndImageRepositoryImplTest.java new file mode 100644 index 000000000..052546a56 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndImageRepositoryImplTest.java @@ -0,0 +1,44 @@ +package com.ddang.ddang.chat.infrastructure.persistence; + +import com.ddang.ddang.chat.infrastructure.persistence.dto.ChatRoomAndImageDto; +import com.ddang.ddang.chat.infrastructure.persistence.fixture.QuerydslChatRoomAndImageRepositoryImplFixture; +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import java.util.Optional; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class QuerydslChatRoomAndImageRepositoryImplTest extends QuerydslChatRoomAndImageRepositoryImplFixture { + + QuerydslChatRoomAndImageRepository querydslChatRoomAndImageRepository; + + @BeforeEach + void setUp(@Autowired final JPAQueryFactory queryFactory) { + querydslChatRoomAndImageRepository = new QuerydslChatRoomAndImageRepositoryImpl(queryFactory); + } + + @Test + void 지정한_채팅방_아이디에_해당하는_채팅방을_조회한다() { + // when + final Optional actual = querydslChatRoomAndImageRepository.findChatRoomById(채팅방.getId()); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isPresent(); + softAssertions.assertThat(actual.get().chatRoom()).isEqualTo(채팅방); + softAssertions.assertThat(actual.get().thumbnailImage()).isEqualTo(경매_대표_이미지); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepositoryImplTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepositoryImplTest.java new file mode 100644 index 000000000..ed1804b90 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepositoryImplTest.java @@ -0,0 +1,52 @@ +package com.ddang.ddang.chat.infrastructure.persistence; + +import com.ddang.ddang.chat.infrastructure.persistence.dto.ChatRoomAndMessageAndImageDto; +import com.ddang.ddang.chat.infrastructure.persistence.fixture.QuerydslChatRoomAndMessageAndImageRepositoryImplFixture; +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import java.util.List; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class QuerydslChatRoomAndMessageAndImageRepositoryImplTest extends QuerydslChatRoomAndMessageAndImageRepositoryImplFixture { + + QuerydslChatRoomAndMessageAndImageRepository querydslChatRoomAndMessageAndImageRepository; + + @BeforeEach + void setUp(@Autowired final JPAQueryFactory queryFactory) { + querydslChatRoomAndMessageAndImageRepository = new QuerydslChatRoomAndMessageAndImageRepositoryImpl(queryFactory); + } + + @Test + void 지정한_사용자_아이디가_포함된_채팅방을_조회한다() { + // when + final List actual = + querydslChatRoomAndMessageAndImageRepository.findAllChatRoomInfoByUserIdOrderByLastMessage(엔초.getId()); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.get(0).chatRoom()).isEqualTo(엔초_지토_채팅방); + softAssertions.assertThat(actual.get(0).message()).isEqualTo(엔초가_지토에게_5시에_보낸_쪽지); + softAssertions.assertThat(actual.get(0).thumbnailImage()).isEqualTo(엔초의_경매_대표_이미지); + softAssertions.assertThat(actual.get(1).chatRoom()).isEqualTo(제이미_엔초_채팅방); + softAssertions.assertThat(actual.get(1).message()).isEqualTo(제이미가_엔초에게_4시에_보낸_쪽지); + softAssertions.assertThat(actual.get(1).thumbnailImage()).isEqualTo(제이미의_경매_대표_이미지); + softAssertions.assertThat(actual.get(2).chatRoom()).isEqualTo(메리_엔초_채팅방); + softAssertions.assertThat(actual.get(2).message()).isEqualTo(메리가_엔초에게_3시에_보낸_쪽지); + softAssertions.assertThat(actual.get(2).thumbnailImage()).isEqualTo(메리의_경매_대표_이미지); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomRepositoryImplTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomRepositoryImplTest.java index 3b82e8008..b9bf60cd8 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomRepositoryImplTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomRepositoryImplTest.java @@ -1,19 +1,8 @@ package com.ddang.ddang.chat.infrastructure.persistence; -import com.ddang.ddang.auction.domain.Auction; -import com.ddang.ddang.auction.domain.BidUnit; -import com.ddang.ddang.auction.domain.Price; -import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; -import com.ddang.ddang.category.domain.Category; -import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; -import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.infrastructure.persistence.fixture.QuerydslChatRoomRepositoryImplFixture; import com.ddang.ddang.configuration.JpaConfiguration; import com.ddang.ddang.configuration.QuerydslConfiguration; -import com.ddang.ddang.user.domain.User; -import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -21,211 +10,25 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -import java.time.LocalDateTime; -import java.util.List; import java.util.Optional; +import static org.assertj.core.api.Assertions.assertThat; + @DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -@Import({JpaConfiguration.class, QuerydslConfiguration.class}) -class QuerydslChatRoomRepositoryImplTest { - - @PersistenceContext - EntityManager em; - - @Autowired - JpaAuctionRepository auctionRepository; - - @Autowired - JpaUserRepository userRepository; - - @Autowired - JpaCategoryRepository categoryRepository; +class QuerydslChatRoomRepositoryImplTest extends QuerydslChatRoomRepositoryImplFixture { @Autowired JpaChatRoomRepository chatRoomRepository; @Test - void 지정한_사용자_아이디가_포함된_채팅방을_조회한다() { - // given - final Category main = new Category("메인"); - final Category sub = new Category("서브"); - main.addSubCategory(sub); - categoryRepository.save(main); - - final User merry = User.builder() - .name("메리") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User encho = User.builder() - .name("엔초") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - final User jamie = User.builder() - .name("제이미") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12347") - .build(); - final User zeeto = User.builder() - .name("지토") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12348") - .build(); - - userRepository.save(merry); - userRepository.save(encho); - userRepository.save(jamie); - userRepository.save(zeeto); - - final Auction merryAuction = Auction.builder() - .title("경매 1") - .seller(merry) - .subCategory(sub) - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(10_000)) - .build(); - final Auction enchoAuction = Auction.builder() - .title("경매 2") - .seller(encho) - .subCategory(sub) - .bidUnit(new BidUnit(2_000)) - .startPrice(new Price(20_000)) - .build(); - final Auction jamieAuction = Auction.builder() - .title("경매 3") - .seller(jamie) - .subCategory(sub) - .bidUnit(new BidUnit(3_000)) - .startPrice(new Price(30_000)) - .build(); - - auctionRepository.save(merryAuction); - auctionRepository.save(enchoAuction); - auctionRepository.save(jamieAuction); - - final ChatRoom merryZeeto = new ChatRoom(merryAuction, zeeto); - final ChatRoom enchoZeeto = new ChatRoom(enchoAuction, zeeto); - final ChatRoom jamieEncho = new ChatRoom(jamieAuction, encho); - chatRoomRepository.save(merryZeeto); - chatRoomRepository.save(enchoZeeto); - chatRoomRepository.save(jamieEncho); - - em.flush(); - em.clear(); - - // when - final List actual = chatRoomRepository.findAllByUserId(encho.getId()); - - // then - SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual).hasSize(2); - softAssertions.assertThat(actual.get(1).getId()).isEqualTo(enchoZeeto.getId()); - softAssertions.assertThat(actual.get(0).getId()).isEqualTo(jamieEncho.getId()); - }); - } - - @Test - void 지정한_채팅방_아이디에_해당하는_채팅방을_조회한다() { - // given - final Category main = new Category("메인"); - final Category sub = new Category("서브"); - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User buyer = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - userRepository.save(seller); - userRepository.save(buyer); - - final Auction auction = Auction.builder() - .title("경매 1") - .seller(seller) - .subCategory(sub) - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(10_000)) - .closingTime(LocalDateTime.now().minusDays(3L)) - .build(); - auctionRepository.save(auction); - - final ChatRoom chatRoom = new ChatRoom(auction, buyer); - chatRoomRepository.save(chatRoom); - - em.flush(); - em.clear(); - - // when - final Optional actual = chatRoomRepository.findChatRoomById(chatRoom.getId()); - - // then - SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual).isPresent(); - softAssertions.assertThat(actual.get()).isEqualTo(chatRoom); - }); - } - - @Test - void 지정한_경매_아이디가_포함된_채팅방을_조회한다() { - // given - final Category main = new Category("메인"); - final Category sub = new Category("서브"); - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("회원1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final User buyer = User.builder() - .name("회원2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - userRepository.save(seller); - userRepository.save(buyer); - - final Auction auction = Auction.builder() - .title("경매 1") - .seller(seller) - .subCategory(sub) - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(10_000)) - .closingTime(LocalDateTime.now().minusDays(3L)) - .build(); - auctionRepository.save(auction); - - final ChatRoom chatRoom = new ChatRoom(auction, buyer); - chatRoomRepository.save(chatRoom); - - em.flush(); - em.clear(); - + void 지정한_경매_아이디가_포함된_채팅방의_아이디를_조회한다() { // when - final Optional actual = chatRoomRepository.findByAuctionId(auction.getId()); + final Optional actual = chatRoomRepository.findChatRoomIdByAuctionId(경매.getId()); // then - SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual).isPresent(); - softAssertions.assertThat(actual.get()).isEqualTo(chatRoom); - }); + assertThat(actual).contains(채팅방.getId()); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslMessageRepositoryImplTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslMessageRepositoryImplTest.java index fc1677b9e..bcf573dc7 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslMessageRepositoryImplTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslMessageRepositoryImplTest.java @@ -1,20 +1,12 @@ package com.ddang.ddang.chat.infrastructure.persistence; -import com.ddang.ddang.auction.domain.Auction; -import com.ddang.ddang.auction.domain.BidUnit; -import com.ddang.ddang.auction.domain.Price; -import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; -import com.ddang.ddang.category.domain.Category; -import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; -import com.ddang.ddang.chat.domain.ChatRoom; import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.chat.infrastructure.persistence.fixture.QuerydslMessageRepositoryImplFixture; import com.ddang.ddang.configuration.JpaConfiguration; import com.ddang.ddang.configuration.QuerydslConfiguration; -import com.ddang.ddang.user.domain.User; -import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; +import com.querydsl.jpa.impl.JPAQueryFactory; import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -22,94 +14,77 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -import java.time.LocalDateTime; -import java.util.Optional; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") @Import({JpaConfiguration.class, QuerydslConfiguration.class}) -class QuerydslMessageRepositoryImplTest { - - @PersistenceContext - EntityManager em; - - @Autowired - JpaAuctionRepository auctionRepository; +class QuerydslMessageRepositoryImplTest extends QuerydslMessageRepositoryImplFixture { - @Autowired - JpaUserRepository userRepository; + QuerydslMessageRepositoryImpl querydslMessageRepository; - @Autowired - JpaCategoryRepository categoryRepository; - - @Autowired - JpaChatRoomRepository chatRoomRepository; - - @Autowired - JpaMessageRepository messageRepository; + @BeforeEach + void setUp(@Autowired final JPAQueryFactory queryFactory) { + querydslMessageRepository = new QuerydslMessageRepositoryImpl(queryFactory); + } @Test - void 주어진_채팅방_아이디에_해당하는_채팅방에_있는_마지막_메시지를_조회한다() { - // given - final Category main = new Category("메인"); - final Category sub = new Category("서브"); - main.addSubCategory(sub); - categoryRepository.save(main); - - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(5.0d) - .oauthId("12345") - .build(); - final User buyer = User.builder() - .name("구매자") - .profileImage("profile.png") - .reliability(5.0d) - .oauthId("12346") - .build(); - userRepository.save(seller); - userRepository.save(buyer); - - final Auction auction = Auction.builder() - .title("경매1") - .seller(seller) - .subCategory(sub) - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(10_000)) - .closingTime(LocalDateTime.now().minusDays(3L)) - .build(); - auctionRepository.save(auction); - - final ChatRoom chatRoom = new ChatRoom(auction, buyer); - chatRoomRepository.save(chatRoom); - - final Message message1 = Message.builder() - .chatRoom(chatRoom) - .writer(seller) - .receiver(buyer) - .contents("메시지 1") - .build(); - final Message lastMessage = Message.builder() - .chatRoom(chatRoom) - .writer(buyer) - .receiver(seller) - .contents("마지막 메시지") - .build(); - messageRepository.save(message1); - messageRepository.save(lastMessage); + void 마지막으로_읽은_메시지_이후에_추가된_메시지를_조회한다() { + // when + final List actual = querydslMessageRepository.findMessagesAllByLastMessageId( + 판매자.getId(), + 채팅방.getId(), + 세_번째_메시지.getId() + ); - em.flush(); - em.clear(); + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(7); + softAssertions.assertThat(actual.get(0)).isEqualTo(네_번째_메시지); + softAssertions.assertThat(actual.get(1)).isEqualTo(다섯_번째_메시지); + softAssertions.assertThat(actual.get(2)).isEqualTo(여섯_번째_메시지); + softAssertions.assertThat(actual.get(3)).isEqualTo(일곱_번째_메시지); + softAssertions.assertThat(actual.get(4)).isEqualTo(여덟_번째_메시지); + softAssertions.assertThat(actual.get(5)).isEqualTo(아홉_번째_메시지); + softAssertions.assertThat(actual.get(6)).isEqualTo(열_번째_메시지); + }); + } + @Test + void 상대방이_메시지를_추가한_경우_마지막으로_읽은_메시지_이후의_메시지를_조회한다() { // when - final Optional actual = messageRepository.findLastMessageByChatRoomId(chatRoom.getId()); + final List actual = querydslMessageRepository.findMessagesAllByLastMessageId( + 구매자.getId(), + 채팅방.getId(), + 세_번째_메시지.getId() + ); // then SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual).isPresent(); - softAssertions.assertThat(actual.get()).isEqualTo(lastMessage); + softAssertions.assertThat(actual).hasSize(7); + softAssertions.assertThat(actual.get(0)).isEqualTo(네_번째_메시지); + softAssertions.assertThat(actual.get(1)).isEqualTo(다섯_번째_메시지); + softAssertions.assertThat(actual.get(2)).isEqualTo(여섯_번째_메시지); + softAssertions.assertThat(actual.get(3)).isEqualTo(일곱_번째_메시지); + softAssertions.assertThat(actual.get(4)).isEqualTo(여덟_번째_메시지); + softAssertions.assertThat(actual.get(5)).isEqualTo(아홉_번째_메시지); + softAssertions.assertThat(actual.get(6)).isEqualTo(열_번째_메시지); }); } + + @Test + void 마지막으로_읽은_메시지_이후의_메시지가_없는_경우_빈_리스트를_반환한다() { + // when + final List actual = querydslMessageRepository.findMessagesAllByLastMessageId( + 구매자.getId(), + 채팅방.getId(), + 열_번째_메시지.getId() + ); + + // then + assertThat(actual).hasSize(0); + } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslMessageRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslMessageRepositoryTest.java deleted file mode 100644 index 27825de03..000000000 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslMessageRepositoryTest.java +++ /dev/null @@ -1,177 +0,0 @@ -package com.ddang.ddang.chat.infrastructure.persistence; - -import com.ddang.ddang.auction.domain.Auction; -import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; -import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; -import com.ddang.ddang.chat.domain.ChatRoom; -import com.ddang.ddang.chat.domain.Message; -import com.ddang.ddang.configuration.JpaConfiguration; -import com.ddang.ddang.configuration.QuerydslConfiguration; -import com.ddang.ddang.region.infrastructure.persistence.JpaRegionRepository; -import com.ddang.ddang.user.domain.User; -import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; -import com.querydsl.jpa.impl.JPAQueryFactory; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -@DataJpaTest -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -@SuppressWarnings("NonAscillCharacters") -@Import({JpaConfiguration.class, QuerydslConfiguration.class}) -class QuerydslMessageRepositoryTest { - - @PersistenceContext - EntityManager em; - - @Autowired - JpaUserRepository userRepository; - - @Autowired - JpaAuctionRepository auctionRepository; - - @Autowired - JpaRegionRepository regionRepository; - - @Autowired - JpaCategoryRepository categoryRepository; - - @Autowired - JpaChatRoomRepository chatRoomRepository; - - @Autowired - JpaMessageRepository messageRepository; - - QuerydslMessageRepository querydslMessageRepository; - - @BeforeEach - void setUp(@Autowired final JPAQueryFactory queryFactory) { - querydslMessageRepository = new QuerydslMessageRepositoryImpl(queryFactory); - } - - @Test - void 마지막으로_읽은_메시지_이후에_추가된_메시지를_조회한다() { - // given - final User seller = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("78923") - .build(); - final User buyer = User.builder() - .name("구매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .title("title") - .build(); - - userRepository.save(seller); - userRepository.save(buyer); - auctionRepository.save(auction); - - final ChatRoom chatRoom = new ChatRoom(auction, buyer); - chatRoomRepository.save(chatRoom); - - final int messagesCount = 10; - for (int count = 0; count < messagesCount; count++) { - final Message message = Message.builder() - .chatRoom(chatRoom) - .writer(seller) - .receiver(buyer) - .contents("안녕하세요") - .build(); - - messageRepository.save(message); - } - - em.flush(); - em.clear(); - - // when - final Long lastMessageId = 3L; - final List messages = messageRepository.findMessagesAllByLastMessageId( - seller.getId(), - chatRoom.getId(), - lastMessageId - ); - - // then - assertThat(messages).hasSizeGreaterThanOrEqualTo(7); - } - - @Test - void 상대방이_메시지를_추가한_경우_마지막으로_읽은_메시지_이후의_메시지를_조회한다() { - // given - final User writer = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("78923") - .build(); - final User receiver = User.builder() - .name("구매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(writer); - userRepository.save(receiver); - - final Auction auction = Auction.builder() - .title("title") - .build(); - - auctionRepository.save(auction); - - final ChatRoom chatRoom = new ChatRoom(auction, receiver); - - chatRoomRepository.save(chatRoom); - - final int messagesCount = 10; - for (int count = 0; count < messagesCount; count++) { - final Message message = Message.builder() - .chatRoom(chatRoom) - .writer(writer) - .receiver(receiver) - .contents("안녕하세요") - .build(); - - messageRepository.save(message); - } - - em.flush(); - em.clear(); - - // when - final Long lastMessageId = 3L; - final List messages = messageRepository.findMessagesAllByLastMessageId( - receiver.getId(), - chatRoom.getId(), - lastMessageId - ); - - // then - assertThat(messages).hasSizeGreaterThanOrEqualTo(7); - } - - @Test - void 조회하려는_메시지_아이디가_존재하지_않는_경우_거짓을_반환한다() { - final Long invalidMessageId = -999L; - - assertThat(messageRepository.existsById(invalidMessageId)).isFalse(); - } -} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/JpaChatRoomRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/JpaChatRoomRepositoryFixture.java new file mode 100644 index 000000000..d1785b0a9 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/JpaChatRoomRepositoryFixture.java @@ -0,0 +1,105 @@ +package com.ddang.ddang.chat.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.bid.domain.Bid; +import com.ddang.ddang.bid.domain.BidPrice; +import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class JpaChatRoomRepositoryFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaCategoryRepository categoryRepository; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaBidRepository bidRepository; + + @Autowired + private JpaChatRoomRepository chatRoomRepository; + + protected User 판매자; + protected User 구매자; + protected Auction 경매; + private Bid 입찰; + protected ChatRoom 채팅방; + protected Long 존재하지_않는_채팅방_아이디 = -999L; + + @BeforeEach + void setUp() { + final Category 전자기기_카테고리 = new Category("전자기기"); + final Category 전자기기_서브_노트북_카테고리 = new Category("노트북 카테고리"); + final ProfileImage 프로필_이미지 = new ProfileImage("upload.png", "store.png"); + final AuctionImage 경매이미지1 = new AuctionImage("경매이미지1.png", "경매이미지1.png"); + final AuctionImage 경매이미지2 = new AuctionImage("경매이미지2.png", "경매이미지2.png"); + + 판매자 = User.builder() + .name("판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + 구매자 = User.builder() + .name("구매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + 경매 = Auction.builder() + .seller(판매자) + .title("맥북") + .description("맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + + 입찰 = new Bid(경매, 구매자, new BidPrice(15_000)); + + 채팅방 = new ChatRoom(경매, 구매자); + + 전자기기_카테고리.addSubCategory(전자기기_서브_노트북_카테고리); + categoryRepository.save(전자기기_카테고리); + + userRepository.saveAll(List.of(판매자, 구매자)); + + 경매.addAuctionImages(List.of(경매이미지1, 경매이미지2)); + auctionRepository.save(경매); + + bidRepository.save(입찰); + 경매.updateLastBid(입찰); + + chatRoomRepository.save(채팅방); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/JpaMessageRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/JpaMessageRepositoryFixture.java new file mode 100644 index 000000000..b80f72f97 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/JpaMessageRepositoryFixture.java @@ -0,0 +1,77 @@ +package com.ddang.ddang.chat.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +@SuppressWarnings("NonAsciiCharacters") +public class JpaMessageRepositoryFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaChatRoomRepository chatRoomRepository; + + protected User 채팅참여_판매자; + protected User 채팅참여_구매자; + protected Auction 경매; + protected Message 메시지; + protected Long 유효하지_않은_메시지_아이디 = -999L; + + @BeforeEach + void setUp() { + // given + 채팅참여_판매자 = User.builder() + .name("판매자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + 채팅참여_구매자 = User.builder() + .name("구매자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + + userRepository.save(채팅참여_판매자); + userRepository.save(채팅참여_구매자); + + 경매 = Auction.builder() + .title("경매") + .build(); + + auctionRepository.save(경매); + + final ChatRoom 채팅방 = new ChatRoom(경매, 채팅참여_구매자); + + chatRoomRepository.save(채팅방); + + 메시지 = Message.builder() + .chatRoom(채팅방) + .writer(채팅참여_판매자) + .receiver(채팅참여_구매자) + .contents("안녕하세요") + .build(); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/QuerydslChatRoomAndImageRepositoryImplFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/QuerydslChatRoomAndImageRepositoryImplFixture.java new file mode 100644 index 000000000..15c437d93 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/QuerydslChatRoomAndImageRepositoryImplFixture.java @@ -0,0 +1,96 @@ +package com.ddang.ddang.chat.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.bid.domain.Bid; +import com.ddang.ddang.bid.domain.BidPrice; +import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class QuerydslChatRoomAndImageRepositoryImplFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaCategoryRepository categoryRepository; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaBidRepository bidRepository; + + @Autowired + private JpaChatRoomRepository chatRoomRepository; + + protected AuctionImage 경매_대표_이미지; + protected ChatRoom 채팅방; + + @BeforeEach + void setUp() { + final Category 전자기기_카테고리 = new Category("전자기기"); + final Category 전자기기_서브_노트북_카테고리 = new Category("노트북 카테고리"); + final ProfileImage 프로필_이미지 = new ProfileImage("upload.png", "store.png"); + final User 판매자 = User.builder() + .name("판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + final User 구매자 = User.builder() + .name("구매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + final AuctionImage 대표_이미지가_아닌_경매_이미지 = + new AuctionImage("대표 이미지가_아닌_경매_이미지.png", "대표 이미지가_아닌_경매_이미지.png"); + final Auction 경매 = Auction.builder() + .seller(판매자) + .title("맥북") + .description("맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + final Bid 입찰 = new Bid(경매, 구매자, new BidPrice(15_000)); + + 경매_대표_이미지 = new AuctionImage("경매_대표_이미지.png", "경매_대표_이미지.png"); + 채팅방 = new ChatRoom(경매, 구매자); + + 전자기기_카테고리.addSubCategory(전자기기_서브_노트북_카테고리); + categoryRepository.save(전자기기_카테고리); + userRepository.saveAll(List.of(판매자, 구매자)); + 경매.addAuctionImages(List.of(경매_대표_이미지, 대표_이미지가_아닌_경매_이미지)); + auctionRepository.save(경매); + bidRepository.save(입찰); + 경매.updateLastBid(입찰); + chatRoomRepository.save(채팅방); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/QuerydslChatRoomAndMessageAndImageRepositoryImplFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/QuerydslChatRoomAndMessageAndImageRepositoryImplFixture.java new file mode 100644 index 000000000..e404c5de1 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/QuerydslChatRoomAndMessageAndImageRepositoryImplFixture.java @@ -0,0 +1,203 @@ +package com.ddang.ddang.chat.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.bid.domain.Bid; +import com.ddang.ddang.bid.domain.BidPrice; +import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; +import com.ddang.ddang.chat.infrastructure.persistence.JpaMessageRepository; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class QuerydslChatRoomAndMessageAndImageRepositoryImplFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaCategoryRepository categoryRepository; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaBidRepository bidRepository; + + @Autowired + private JpaChatRoomRepository chatRoomRepository; + + @Autowired + private JpaMessageRepository messageRepository; + + protected User 엔초; + protected AuctionImage 메리의_경매_대표_이미지; + protected AuctionImage 엔초의_경매_대표_이미지; + protected AuctionImage 제이미의_경매_대표_이미지; + protected ChatRoom 메리_엔초_채팅방; + protected ChatRoom 엔초_지토_채팅방; + protected ChatRoom 제이미_엔초_채팅방; + protected Message 메리가_엔초에게_3시에_보낸_쪽지; + protected Message 제이미가_엔초에게_4시에_보낸_쪽지; + protected Message 엔초가_지토에게_5시에_보낸_쪽지; + + @BeforeEach + void setUp() { + final Category 전자기기_카테고리 = new Category("전자기기"); + final Category 전자기기_서브_노트북_카테고리 = new Category("노트북 카테고리"); + final ProfileImage 프로필_이미지 = new ProfileImage("upload.png", "store.png"); + + 엔초 = User.builder() + .name("엔초") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + final User 메리 = User.builder() + .name("메리") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + final User 제이미 = User.builder() + .name("제이미") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + final User 지토 = User.builder() + .name("지토") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12348") + .build(); + + 메리의_경매_대표_이미지 = new AuctionImage("메리의_경매_대표_이미지.png", "메리의_경매_대표_이미지.png"); + final AuctionImage 메리의_대표_이미지가_아닌_경매_이미지 = + new AuctionImage("메리의_대표 이미지가_아닌_경매_이미지.png", "메리의_대표 이미지가_아닌_경매_이미지.png"); + 엔초의_경매_대표_이미지 = new AuctionImage("엔초의_경매_대표_이미지.png", "엔초의_경매_대표_이미지.png"); + final AuctionImage 엔초의_대표_이미지가_아닌_경매_이미지 = + new AuctionImage("엔초의_대표 이미지가_아닌_경매_이미지.png", "엔초의_대표 이미지가_아닌_경매_이미지.png"); + 제이미의_경매_대표_이미지 = new AuctionImage("제이미의_경매_대표_이미지.png", "제이미의_경매_대표_이미지.png"); + final AuctionImage 제이미의_대표_이미지가_아닌_경매_이미지 = + new AuctionImage("제이미의_대표 이미지가_아닌_경매_이미지.png", "제이미의_대표 이미지가_아닌_경매_이미지.png"); + + final Auction 메리의_경매 = Auction.builder() + .seller(메리) + .title("메리 맥북") + .description("메리 맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + final Auction 엔초의_경매 = Auction.builder() + .seller(엔초) + .title("엔초 맥북") + .description("엔초 맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + final Auction 제이미의_경매 = Auction.builder() + .seller(제이미) + .title("제이미 맥북") + .description("제이미 맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + + final Bid 엔초가_메리_경매에_입찰 = new Bid(메리의_경매, 엔초, new BidPrice(15_000)); + final Bid 지토가_엔초_경매에_입찰 = new Bid(엔초의_경매, 지토, new BidPrice(15_000)); + final Bid 엔초가_제이미_경매에_입찰 = new Bid(제이미의_경매, 엔초, new BidPrice(15_000)); + + 메리_엔초_채팅방 = new ChatRoom(메리의_경매, 엔초); + 엔초_지토_채팅방 = new ChatRoom(엔초의_경매, 지토); + 제이미_엔초_채팅방 = new ChatRoom(제이미의_경매, 엔초); + + final Message 제이미가_엔초에게_1시에_보낸_쪽지 = Message.builder() + .chatRoom(제이미_엔초_채팅방) + .contents("제이미가 엔초에게 1시애 보낸 쪽지") + .writer(제이미) + .receiver(엔초) + .build(); + final Message 엔초가_지토에게_2시에_보낸_쪽지 = Message.builder() + .chatRoom(엔초_지토_채팅방) + .contents("엔초가 지토에게 2시애 보낸 쪽지") + .writer(엔초) + .receiver(지토) + .build(); + 메리가_엔초에게_3시에_보낸_쪽지 = Message.builder() + .chatRoom(메리_엔초_채팅방) + .contents("메리가 엔초에게 3시에 보낸 쪽지") + .writer(엔초) + .receiver(지토) + .build(); + 제이미가_엔초에게_4시에_보낸_쪽지 = Message.builder() + .chatRoom(제이미_엔초_채팅방) + .contents("제이미가 엔초에게 4시애 보낸 쪽지") + .writer(제이미) + .receiver(엔초) + .build(); + 엔초가_지토에게_5시에_보낸_쪽지 = Message.builder() + .chatRoom(엔초_지토_채팅방) + .contents("엔초가 지토에게 5시애 보낸 쪽지") + .writer(엔초) + .receiver(지토) + .build(); + + + 전자기기_카테고리.addSubCategory(전자기기_서브_노트북_카테고리); + categoryRepository.save(전자기기_카테고리); + + userRepository.saveAll(List.of(메리, 엔초, 제이미, 지토)); + + 메리의_경매.addAuctionImages(List.of(메리의_경매_대표_이미지, 메리의_대표_이미지가_아닌_경매_이미지)); + 엔초의_경매.addAuctionImages(List.of(엔초의_경매_대표_이미지, 엔초의_대표_이미지가_아닌_경매_이미지)); + 제이미의_경매.addAuctionImages(List.of(제이미의_경매_대표_이미지, 제이미의_대표_이미지가_아닌_경매_이미지)); + auctionRepository.saveAll(List.of(메리의_경매, 엔초의_경매, 제이미의_경매)); + + bidRepository.saveAll(List.of(엔초가_메리_경매에_입찰, 지토가_엔초_경매에_입찰, 엔초가_제이미_경매에_입찰)); + 메리의_경매.updateLastBid(엔초가_메리_경매에_입찰); + 엔초의_경매.updateLastBid(지토가_엔초_경매에_입찰); + 제이미의_경매.updateLastBid(엔초가_제이미_경매에_입찰); + + chatRoomRepository.saveAll(List.of(메리_엔초_채팅방, 엔초_지토_채팅방, 제이미_엔초_채팅방)); + + messageRepository.saveAll( + List.of( + 제이미가_엔초에게_1시에_보낸_쪽지, + 엔초가_지토에게_2시에_보낸_쪽지, + 메리가_엔초에게_3시에_보낸_쪽지, + 제이미가_엔초에게_4시에_보낸_쪽지, + 엔초가_지토에게_5시에_보낸_쪽지 + ) + ); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/QuerydslChatRoomRepositoryImplFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/QuerydslChatRoomRepositoryImplFixture.java new file mode 100644 index 000000000..bef8380e9 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/QuerydslChatRoomRepositoryImplFixture.java @@ -0,0 +1,103 @@ +package com.ddang.ddang.chat.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.bid.domain.Bid; +import com.ddang.ddang.bid.domain.BidPrice; +import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class QuerydslChatRoomRepositoryImplFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaCategoryRepository categoryRepository; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaBidRepository bidRepository; + + @Autowired + private JpaChatRoomRepository chatRoomRepository; + + protected AuctionImage 경매_대표_이미지; + protected Auction 경매; + protected ChatRoom 채팅방; + + @BeforeEach + void setUp() { + final Category 전자기기_카테고리 = new Category("전자기기"); + final Category 전자기기_서브_노트북_카테고리 = new Category("노트북 카테고리"); + final ProfileImage 프로필_이미지 = new ProfileImage("upload.png", "store.png"); + final User 판매자 = User.builder() + .name("판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + final User 구매자 = User.builder() + .name("구매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + final AuctionImage 대표_이미지가_아닌_경매_이미지 = + new AuctionImage("대표 이미지가_아닌_경매_이미지.png", "대표 이미지가_아닌_경매_이미지.png"); + + 경매 = Auction.builder() + .seller(판매자) + .title("맥북") + .description("맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + + final Bid 입찰 = new Bid(경매, 구매자, new BidPrice(15_000)); + + 경매_대표_이미지 = new AuctionImage("경매_대표_이미지.png", "경매_대표_이미지.png"); + 채팅방 = new ChatRoom(경매, 구매자); + + 전자기기_카테고리.addSubCategory(전자기기_서브_노트북_카테고리); + categoryRepository.save(전자기기_카테고리); + + userRepository.saveAll(List.of(판매자, 구매자)); + + 경매.addAuctionImages(List.of(경매_대표_이미지, 대표_이미지가_아닌_경매_이미지)); + auctionRepository.save(경매); + + bidRepository.save(입찰); + 경매.updateLastBid(입찰); + + chatRoomRepository.save(채팅방); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/QuerydslMessageRepositoryImplFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/QuerydslMessageRepositoryImplFixture.java new file mode 100644 index 000000000..fa02b5fb4 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/QuerydslMessageRepositoryImplFixture.java @@ -0,0 +1,107 @@ +package com.ddang.ddang.chat.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; +import com.ddang.ddang.chat.infrastructure.persistence.JpaMessageRepository; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class QuerydslMessageRepositoryImplFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaChatRoomRepository chatRoomRepository; + + @Autowired + private JpaMessageRepository messageRepository; + + protected User 판매자; + protected User 구매자; + protected Auction 경매; + protected ChatRoom 채팅방; + protected int 메시지_총_개수; + protected List 저장된_메시지들; + protected Message 세_번째_메시지; + protected Message 네_번째_메시지; + protected Message 다섯_번째_메시지; + protected Message 여섯_번째_메시지; + protected Message 일곱_번째_메시지; + protected Message 여덟_번째_메시지; + protected Message 아홉_번째_메시지; + protected Message 열_번째_메시지; + + @BeforeEach + void setUp() { + 판매자 = User.builder() + .name("판매자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("78923") + .build(); + 구매자 = User.builder() + .name("구매자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + + userRepository.save(판매자); + userRepository.save(구매자); + + 경매 = Auction.builder() + .title("title") + .build(); + + auctionRepository.save(경매); + + 채팅방 = new ChatRoom(경매, 구매자); + + chatRoomRepository.save(채팅방); + + 메시지_총_개수 = 10; + 저장된_메시지들 = new ArrayList<>(); + for (int count = 0; count < 메시지_총_개수; count++) { + final Message message = Message.builder() + .chatRoom(채팅방) + .writer(판매자) + .receiver(구매자) + .contents("안녕하세요") + .build(); + 저장된_메시지들.add(message); + } + messageRepository.saveAll(저장된_메시지들); + + 세_번째_메시지 = 저장된_메시지들.get(2); + 네_번째_메시지 = 저장된_메시지들.get(3); + 다섯_번째_메시지 = 저장된_메시지들.get(4); + 여섯_번째_메시지 = 저장된_메시지들.get(5); + 일곱_번째_메시지 = 저장된_메시지들.get(6); + 여덟_번째_메시지 = 저장된_메시지들.get(7); + 아홉_번째_메시지 = 저장된_메시지들.get(8); + 열_번째_메시지 = 저장된_메시지들.get(9); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/ChatRoomControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/ChatRoomControllerTest.java index 954f2fefd..76b29dda0 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/ChatRoomControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/ChatRoomControllerTest.java @@ -2,59 +2,34 @@ import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; import com.ddang.ddang.auction.domain.exception.WinnerNotFoundException; -import com.ddang.ddang.authentication.application.AuthenticationUserService; -import com.ddang.ddang.authentication.application.BlackListTokenService; import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; import com.ddang.ddang.authentication.domain.TokenDecoder; import com.ddang.ddang.authentication.domain.TokenType; import com.ddang.ddang.authentication.domain.dto.AuthenticationStore; -import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims; -import com.ddang.ddang.chat.application.ChatRoomService; -import com.ddang.ddang.chat.application.MessageService; import com.ddang.ddang.chat.application.dto.CreateChatRoomDto; import com.ddang.ddang.chat.application.dto.CreateMessageDto; -import com.ddang.ddang.chat.application.dto.ReadAuctionInChatRoomDto; -import com.ddang.ddang.chat.application.dto.ReadChatRoomWithLastMessageDto; -import com.ddang.ddang.chat.application.dto.ReadLastMessageDto; -import com.ddang.ddang.chat.application.dto.ReadMessageDto; -import com.ddang.ddang.chat.application.dto.ReadParticipatingChatRoomDto; -import com.ddang.ddang.chat.application.dto.ReadUserInChatRoomDto; import com.ddang.ddang.chat.application.exception.ChatRoomNotFoundException; import com.ddang.ddang.chat.application.exception.InvalidAuctionToChatException; +import com.ddang.ddang.chat.application.exception.InvalidUserToChat; import com.ddang.ddang.chat.application.exception.MessageNotFoundException; -import com.ddang.ddang.chat.application.exception.UserNotAccessibleException; -import com.ddang.ddang.chat.presentation.dto.request.CreateChatRoomRequest; -import com.ddang.ddang.chat.presentation.dto.request.CreateMessageRequest; +import com.ddang.ddang.chat.application.exception.UnableToChatException; import com.ddang.ddang.chat.presentation.dto.request.ReadMessageRequest; import com.ddang.ddang.chat.presentation.dto.response.ReadMessageResponse; -import com.ddang.ddang.configuration.RestDocsConfiguration; +import com.ddang.ddang.chat.presentation.fixture.ChatRoomControllerFixture; import com.ddang.ddang.exception.GlobalExceptionHandler; import com.ddang.ddang.user.application.exception.UserNotFoundException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; -import org.springframework.context.annotation.Import; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import java.time.LocalDateTime; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -82,52 +57,19 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(controllers = {ChatRoomController.class}, - excludeFilters = { - @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfigurer.class), - @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com\\.ddang\\.ddang\\.authentication\\.configuration\\..*") - } -) -@AutoConfigureRestDocs -@Import(RestDocsConfiguration.class) -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class ChatRoomControllerTest { - - @MockBean - BlackListTokenService blackListTokenService; - - @MockBean - ChatRoomService chatRoomService; - - @MockBean - MessageService messageService; - - @MockBean - AuthenticationUserService authenticationUserService; - - @Autowired - ChatRoomController chatRoomController; - - @Autowired - ObjectMapper objectMapper; - - @Autowired - RestDocumentationResultHandler restDocs; - - TokenDecoder mockTokenDecoder; +class ChatRoomControllerTest extends ChatRoomControllerFixture { + TokenDecoder tokenDecoder = mock(TokenDecoder.class); MockMvc mockMvc; @BeforeEach - void setUp(@Autowired RestDocumentationContextProvider provider) { - mockTokenDecoder = mock(TokenDecoder.class); - + void setUp() { final AuthenticationStore store = new AuthenticationStore(); final AuthenticationInterceptor interceptor = new AuthenticationInterceptor( blackListTokenService, authenticationUserService, - mockTokenDecoder, + tokenDecoder, store ); final AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(store); @@ -145,184 +87,103 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { @Test void 메시지를_생성한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - - final CreateMessageRequest request = new CreateMessageRequest(1L, "메시지 내용"); - - given(messageService.create(any(CreateMessageDto.class))).willReturn(1L); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(messageService.create(any(CreateMessageDto.class), anyString())).willReturn(채팅방_아이디); // when & then - mockMvc.perform(RestDocumentationRequestBuilders.post("/chattings/{chatRoomId}/messages", 1L) - .contentType(MediaType.APPLICATION_JSON) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .content(objectMapper.writeValueAsString(request))) - .andExpectAll( - status().isCreated(), - header().string(HttpHeaders.LOCATION, is("/chattings/1")), - jsonPath("$.id", is(1L), Long.class) - ) - .andDo( - restDocs.document( - requestHeaders( - headerWithName("Authorization").description("회원 Bearer 인증 정보") - ), - pathParameters( - parameterWithName("chatRoomId").description("메시지를 보내고 싶은 채팅방의 ID") - ), - requestFields( - fieldWithPath("receiverId").description("메시지 수신자 ID"), - fieldWithPath("contents").description("메시지 내용") - ), - responseFields( - fieldWithPath("id").type(JsonFieldType.NUMBER).description("메시지 보내진 채팅방 ID") - ) - ) - ); + final ResultActions resultActions = mockMvc.perform(RestDocumentationRequestBuilders.post("/chattings/{chatRoomId}/messages", 채팅방_아이디) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .content(objectMapper.writeValueAsString(메시지_생성_요청))) + .andExpectAll( + status().isCreated(), + header().string(HttpHeaders.LOCATION, is("/chattings/1")), + jsonPath("$.id", is(1L), Long.class) + ); + createMessage_문서화(resultActions); } @Test void 채팅방이_없는_경우_메시지_생성시_404를_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - - final Long invalidChatRoomId = -999L; - final CreateMessageRequest request = new CreateMessageRequest(1L, "메시지 내용"); - - final ChatRoomNotFoundException chatRoomNotFoundException = - new ChatRoomNotFoundException("지정한 아이디에 대한 채팅방을 찾을 수 없습니다."); - - given(messageService.create(any(CreateMessageDto.class))).willThrow(chatRoomNotFoundException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(messageService.create(any(CreateMessageDto.class), anyString())).willThrow(new ChatRoomNotFoundException("지정한 아이디에 대한 채팅방을 찾을 수 없습니다.")); // when & then - mockMvc.perform(post("/chattings/{chatRoomId}/messages", invalidChatRoomId) + mockMvc.perform(post("/chattings/{chatRoomId}/messages", 유효하지_않은_채팅방_아이디) .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .content(objectMapper.writeValueAsString(request)) + .content(objectMapper.writeValueAsString(메시지_생성_요청)) .contentType(MediaType.APPLICATION_JSON)) .andExpectAll( status().isNotFound(), - jsonPath("$.message", is(chatRoomNotFoundException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 발신자가_없는_경우_메시지_생성시_404를_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - - final Long invalidWriterId = -999L; - final Long chatRoomId = 1L; - final CreateMessageRequest request = new CreateMessageRequest(invalidWriterId, "메시지 내용"); - - final UserNotFoundException userNotFoundException = new UserNotFoundException( - "지정한 아이디에 대한 발신자를 찾을 수 없습니다." - ); - - given(messageService.create(any(CreateMessageDto.class))).willThrow(userNotFoundException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(messageService.create(any(CreateMessageDto.class), anyString())).willThrow(new UserNotFoundException("지정한 아이디에 대한 발신자를 찾을 수 없습니다.")); // when & then - mockMvc.perform(post("/chattings/{chatRoomId}/messages", chatRoomId) + mockMvc.perform(post("/chattings/{chatRoomId}/messages", 채팅방_아이디) .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .content(objectMapper.writeValueAsString(request)) + .content(objectMapper.writeValueAsString(유효하지_않은_발신자의_메시지_생성_요청)) .contentType(MediaType.APPLICATION_JSON)) .andExpectAll( status().isNotFound(), - jsonPath("$.message", is(userNotFoundException.getMessage())) + jsonPath("$.message").exists() ); } @Test - void 마지막_조회_메시지_이후_메시지를_조회한다() throws Exception { + void 발신자가_탈퇴한_사용자인_경우_메시지_생성시_400를_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - - final Long lastMessageId = 1L; - final ReadUserInChatRoomDto writerDto = new ReadUserInChatRoomDto(1L, "user", "profile.png", 5.0d); - final ReadUserInChatRoomDto receiverDto = new ReadUserInChatRoomDto(1L, "user", "profile.png", 5.0d); - - final ReadAuctionInChatRoomDto auctionDto = new ReadAuctionInChatRoomDto( - 1L, - "경매1", - 10_000, - List.of(1L, 2L), - "main", - "sub", - writerDto.id(), - writerDto.profileImage(), - writerDto.name(), - writerDto.reliability() - ); - - final ReadParticipatingChatRoomDto chatRoomDto = new ReadParticipatingChatRoomDto( - 1L, - auctionDto, - writerDto, - true - ); - - final ReadMessageDto readMessageDto = new ReadMessageDto( - 1L, - LocalDateTime.now(), - chatRoomDto, - writerDto, - receiverDto, - "메시지내용" - ); - final ReadMessageResponse expected = new ReadMessageResponse(1L, LocalDateTime.now(), true, "메시지내용"); - - given(messageService.readAllByLastMessageId(any(ReadMessageRequest.class))).willReturn(List.of(readMessageDto)); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(messageService.create(any(CreateMessageDto.class), anyString())).willThrow(new UnableToChatException("탈퇴한 사용자에게는 메시지 전송이 불가능합니다.")); // when & then - mockMvc.perform(RestDocumentationRequestBuilders.get("/chattings/{chatRoomId}/messages", 1L) + mockMvc.perform(post("/chattings/{chatRoomId}/messages", 채팅방_아이디) .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON) - .queryParam("lastMessageId", lastMessageId.toString()) - ) + .content(objectMapper.writeValueAsString(탈퇴한_사용자와의_메시지_생성_요청)) + .contentType(MediaType.APPLICATION_JSON)) .andExpectAll( - status().isOk(), - jsonPath("$.[0].isMyMessage", is(expected.isMyMessage())), - jsonPath("$.[0].contents", is(expected.contents())) - ) - .andDo( - restDocs.document( - requestHeaders( - headerWithName("Authorization").description("회원 Bearer 인증 정보") - ), - pathParameters( - parameterWithName("chatRoomId").description("메시지를 보내고 싶은 채팅방의 ID") - ), - queryParameters( - parameterWithName("lastMessageId").description("마지막으로 응답받은 메시지의 ID").optional() - ), - responseFields( - fieldWithPath("[]").type(JsonFieldType.ARRAY).description("하나의 채팅방 내의 메시지 목록 (lastMessageId가 포함되어 있다면 lastMessageId 이후의 메시지 목록"), - fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("메시지 ID"), - fieldWithPath("[].createdAt").type(JsonFieldType.STRING).description("메시지를 보낸 시간"), - fieldWithPath("[].isMyMessage").type(JsonFieldType.BOOLEAN).description("조회를 요청한 사람이 보낸 메시지인지 여부"), - fieldWithPath("[].contents").type(JsonFieldType.STRING).description("메시지 내용") - ) - ) - ); + status().isBadRequest(), + jsonPath("$.message").exists() + ); } @Test - void 마지막_메시지_아이디가_없는_경우_빈_리스트를_반환한다() throws Exception { + void 마지막_조회_메시지_이후_메시지를_조회한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(messageService.readAllByLastMessageId(any(ReadMessageRequest.class))).willReturn(List.of(조회용_메시지)); - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); + final ReadMessageResponse expected = ReadMessageResponse.of(조회용_메시지, true); + // when & then + final ResultActions resultActions = mockMvc.perform(RestDocumentationRequestBuilders.get("/chattings/{chatRoomId}/messages", 채팅방_아이디) + .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .contentType(MediaType.APPLICATION_JSON) + .queryParam("lastMessageId", 마지막_메시지_아이디.toString()) + ) + .andExpectAll( + status().isOk(), + jsonPath("$.[0].isMyMessage", is(expected.isMyMessage())), + jsonPath("$.[0].contents", is(expected.contents())) + ); + readAllByLastMessageId_문서화(resultActions); + } + + @Test + void 마지막_메시지_아이디가_없는_경우_빈_리스트를_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); given(messageService.readAllByLastMessageId(any(ReadMessageRequest.class))).willReturn(Collections.emptyList()); // when & then - mockMvc.perform(get("/chattings/1/messages") + mockMvc.perform(get("/chattings/{chatRoomId}/messages", 채팅방_아이디) .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") .contentType(MediaType.APPLICATION_JSON) ) @@ -335,154 +196,72 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { @Test void 채팅방_아이디가_잘못된_경우_메시지를_조회하면_404를_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - - final Long invalidChatRoomId = -999L; - final ChatRoomNotFoundException chatRoomNotFoundException = - new ChatRoomNotFoundException("지정한 아이디에 대한 채팅방을 찾을 수 없습니다."); - - given(messageService.readAllByLastMessageId(any(ReadMessageRequest.class))).willThrow(chatRoomNotFoundException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(messageService.readAllByLastMessageId(any(ReadMessageRequest.class))).willThrow(new ChatRoomNotFoundException("지정한 아이디에 대한 채팅방을 찾을 수 없습니다.")); // when & then - mockMvc.perform(get("/chattings/" + invalidChatRoomId + "/messages") + mockMvc.perform(get("/chattings/{chatRoomId}/messages", 유효하지_않은_채팅방_아이디) .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") .contentType(MediaType.APPLICATION_JSON) - .queryParam("lastMessageId", "1")) + .queryParam("lastMessageId", 마지막_메시지_아이디.toString())) .andExpectAll( status().isNotFound(), - jsonPath("$.message", is(chatRoomNotFoundException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 마지막_메시지_아이디가_잘못된_경우_메시지를_조회하면_404를_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - - final Long invalidMessageId = -999L; - final MessageNotFoundException messageNotFoundException = - new MessageNotFoundException("조회한 마지막 메시지가 존재하지 않습니다."); - - given(messageService.readAllByLastMessageId(any(ReadMessageRequest.class))).willThrow(messageNotFoundException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(messageService.readAllByLastMessageId(any(ReadMessageRequest.class))).willThrow(new MessageNotFoundException("조회한 마지막 메시지가 존재하지 않습니다.")); // when & then - mockMvc.perform(get("/chattings/1/messages") + mockMvc.perform(get("/chattings/{chatRoomId}/messages", 채팅방_아이디) .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") .contentType(MediaType.APPLICATION_JSON) - .queryParam("lastMessageId", invalidMessageId.toString()) + .queryParam("lastMessageId", 유효하지_않은_마지막_메시지_아이디.toString()) ) .andExpectAll( status().isNotFound(), - jsonPath("$.message", is(messageNotFoundException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 사용자가_참여한_모든_채팅방을_조회한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - - ReadUserInChatRoomDto seller = new ReadUserInChatRoomDto(1L, "사용자1", "profile.png", 5.0d); - final ReadUserInChatRoomDto buyer1 = new ReadUserInChatRoomDto(2L, "사용자2", "profile.png", 5.0d); - final ReadUserInChatRoomDto buyer2 = new ReadUserInChatRoomDto(3L, "사용자3", "profile.png", 5.0d); - final ReadAuctionInChatRoomDto auctionDto1 = new ReadAuctionInChatRoomDto( - 1L, - "경매1", - 10_000, - List.of(1L, 2L), - "main", - "sub", - seller.id(), - seller.profileImage(), - seller.name(), - seller.reliability() - ); - final ReadChatRoomWithLastMessageDto dto1 = new ReadChatRoomWithLastMessageDto( - 1L, - auctionDto1, - buyer1, - new ReadLastMessageDto(1L, LocalDateTime.now(), seller, buyer1, "메시지1"), - true - ); - final ReadAuctionInChatRoomDto auctionDto2 = new ReadAuctionInChatRoomDto( - 2L, - "경매2", - 20_000, - List.of(1L, 2L), - "main", - "sub", - seller.id(), - seller.profileImage(), - seller.name(), - seller.reliability() - ); - final ReadChatRoomWithLastMessageDto dto2 = new ReadChatRoomWithLastMessageDto( - 2L, - auctionDto2, - buyer2, - new ReadLastMessageDto(1L, LocalDateTime.now(), seller, buyer2, "메시지2"), - true - ); - - given(chatRoomService.readAllByUserId(anyLong())) - .willReturn(List.of(dto1, dto2)); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(chatRoomService.readAllByUserId(anyLong())).willReturn(List.of(조회용_채팅방1, 조회용_채팅방2)); // when & then - mockMvc.perform(get("/chattings") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON)) - .andExpectAll( - status().isOk(), - jsonPath("$.[0].id", is(dto1.id()), Long.class), - jsonPath("$.[0].chatPartner.name", is(dto1.partnerDto().name())), - jsonPath("$.[0].auction.title", is(dto1.auctionDto().title())), - jsonPath("$.[0].lastMessage.contents", is(dto1.lastMessageDto().contents())), - jsonPath("$.[1].id", is(dto2.id()), Long.class), - jsonPath("$.[1].chatPartner.name", is(dto2.partnerDto().name())), - jsonPath("$.[1].auction.title", is(dto2.auctionDto().title())), - jsonPath("$.[1].lastMessage.contents", is(dto2.lastMessageDto().contents())) - ) - .andDo( - restDocs.document( - requestHeaders( - headerWithName("Authorization").description("회원 Bearer 인증 정보") - ), - responseFields( - fieldWithPath("[]").type(JsonFieldType.ARRAY).description("자신이 참여한 채팅방 목록"), - fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("채팅방 ID"), - fieldWithPath("[].chatPartner").type(JsonFieldType.OBJECT).description("채팅 상대방"), - fieldWithPath("[].chatPartner.id").type(JsonFieldType.NUMBER).description("채팅 상대방 ID"), - fieldWithPath("[].chatPartner.name").type(JsonFieldType.STRING).description("채팅 상대방 이름"), - fieldWithPath("[].chatPartner.profileImage").type(JsonFieldType.STRING).description("채팅 상대방 프로필 사진"), - fieldWithPath("[].auction").type(JsonFieldType.OBJECT).description("채팅방과 연관된 경매"), - fieldWithPath("[].auction.id").type(JsonFieldType.NUMBER).description("경매 ID"), - fieldWithPath("[].auction.title").type(JsonFieldType.STRING).description("경매 제목"), - fieldWithPath("[].auction.image").type(JsonFieldType.STRING).description("경매 대표 사진"), - fieldWithPath("[].auction.price").type(JsonFieldType.NUMBER).description("낙찰가"), - fieldWithPath("[].lastMessage").type(JsonFieldType.OBJECT).description("마지막으로 전송된 메시지"), - fieldWithPath("[].lastMessage.createdAt").type(JsonFieldType.STRING).description("메시지를 보낸 시간"), - fieldWithPath("[].lastMessage.contents").type(JsonFieldType.STRING).description("메시지 내용"), - fieldWithPath("[].isChatAvailable").type(JsonFieldType.BOOLEAN).description("채팅 가능 여부") - ) - ) - ); + final ResultActions resultActions = mockMvc.perform(get("/chattings") + .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .contentType(MediaType.APPLICATION_JSON)) + .andExpectAll( + status().isOk(), + jsonPath("$.[0].id", is(조회용_채팅방1.id()), Long.class), + jsonPath("$.[0].chatPartner.name", is(조회용_채팅방1.partnerDto() + .name())), + jsonPath("$.[0].auction.title", is(조회용_채팅방1.auctionDto() + .title())), + jsonPath("$.[0].lastMessage.contents", is(조회용_채팅방1.lastMessageDto() + .contents())), + jsonPath("$.[1].id", is(조회용_채팅방2.id()), Long.class), + jsonPath("$.[1].chatPartner.name", is(조회용_채팅방2.partnerDto() + .name())), + jsonPath("$.[1].auction.title", is(조회용_채팅방2.auctionDto() + .title())), + jsonPath("$.[1].lastMessage.contents", is(조회용_채팅방2.lastMessageDto() + .contents())) + ); + readAllParticipatingChatRooms_문서화(resultActions); } @Test void 사용자가_참여한_채팅방_목록_조회시_요청한_사용자_정보가_없다면_404를_반환한다() throws Exception { // given - final UserNotFoundException userNotFoundException = new UserNotFoundException("사용자 정보를 찾을 수 없습니다."); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willThrow(userNotFoundException); - - final Long invalidUserId = -999L; - given(chatRoomService.readAllByUserId(invalidUserId)).willThrow(userNotFoundException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willThrow(new UserNotFoundException("사용자 정보를 찾을 수 없습니다.")); // when & then mockMvc.perform(get("/chattings") @@ -490,321 +269,329 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { .contentType(MediaType.APPLICATION_JSON)) .andExpectAll( status().isNotFound(), - jsonPath("$.message", is(userNotFoundException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 지정한_아이디에_해당하는_채팅방을_조회한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - - final ReadAuctionInChatRoomDto auction = new ReadAuctionInChatRoomDto( - 1L, - "경매 상품 1", - 3_000, - List.of(1L, 2L), - "메인 카테고리", - "서브 카테고리", - 1L, - "profile.png", - "판매자", - 5.0d - ); - final ReadUserInChatRoomDto chatPartner = new ReadUserInChatRoomDto( - 2L, - "채팅 상대방", - "profile.png", - 5.0 - ); - - final ReadParticipatingChatRoomDto chatRoom = new ReadParticipatingChatRoomDto( - 1L, - auction, - chatPartner, - true - ); - - given(chatRoomService.readByChatRoomId(anyLong(), anyLong())).willReturn(chatRoom); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(chatRoomService.readByChatRoomId(anyLong(), anyLong())).willReturn(조회용_참가중인_채팅방); // when & then - mockMvc.perform(RestDocumentationRequestBuilders.get("/chattings/{chatRoomId}", 1L) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON)) - .andExpectAll( - status().isOk(), - jsonPath("$.id", is(chatRoom.id()), Long.class), - jsonPath("$.chatPartner.name", is(chatRoom.partnerDto().name())), - jsonPath("$.auction.title", is(chatRoom.auctionDto().title())) - ) - .andDo( - restDocs.document( - requestHeaders( - headerWithName("Authorization").description("회원 Bearer 인증 정보") - ), - pathParameters( - parameterWithName("chatRoomId").description("조회하고자 하는 채팅방 ID") - ), - responseFields( - fieldWithPath("id").type(JsonFieldType.NUMBER).description("채팅방 ID"), - fieldWithPath("auction").type(JsonFieldType.OBJECT).description("채팅방과 연관된 경매"), - fieldWithPath("auction.id").type(JsonFieldType.NUMBER).description("경매 ID"), - fieldWithPath("auction.title").type(JsonFieldType.STRING).description("경매 제목"), - fieldWithPath("auction.image").type(JsonFieldType.STRING).description("경매 대표 사진"), - fieldWithPath("auction.price").type(JsonFieldType.NUMBER).description("낙찰가"), - fieldWithPath("chatPartner").type(JsonFieldType.OBJECT).description("채팅 상대방"), - fieldWithPath("chatPartner.id").type(JsonFieldType.NUMBER).description("채팅 상대방 ID"), - fieldWithPath("chatPartner.name").type(JsonFieldType.STRING).description("채팅 상대방 이름"), - fieldWithPath("chatPartner.profileImage").type(JsonFieldType.STRING).description("채팅 상대방 프로필 사진"), - fieldWithPath("isChatAvailable").type(JsonFieldType.BOOLEAN).description("채팅 가능 여부") - ) - ) - ); + final ResultActions resultActions = mockMvc.perform(RestDocumentationRequestBuilders.get("/chattings/{chatRoomId}", 채팅방_아이디) + .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .contentType(MediaType.APPLICATION_JSON)) + .andExpectAll( + status().isOk(), + jsonPath("$.id", is(조회용_참가중인_채팅방.id()), Long.class), + jsonPath("$.chatPartner.name", is(조회용_참가중인_채팅방.partnerDto() + .name())), + jsonPath("$.auction.title", is(조회용_참가중인_채팅방.auctionDto() + .title())) + ); + readChatRoom_문서화(resultActions); } @Test void 지정한_아이디에_해당하는_채팅방_조회시_요청한_사용자_정보가_없다면_404를_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - - final UserNotFoundException userNotFoundException = new UserNotFoundException("사용자 정보를 찾을 수 없습니다."); - - given(chatRoomService.readByChatRoomId(anyLong(), anyLong())).willThrow(userNotFoundException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(chatRoomService.readByChatRoomId(anyLong(), anyLong())).willThrow(new UserNotFoundException("사용자 정보를 찾을 수 없습니다.")); // when & then - mockMvc.perform(get("/chattings/1") + mockMvc.perform(get("/chattings/{chatRoomId}", 채팅방_아이디) .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") .contentType(MediaType.APPLICATION_JSON)) .andExpectAll( status().isNotFound(), - jsonPath("$.message", is(userNotFoundException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 지정한_아이디에_해당하는_채팅방_조회시_채팅방을_찾을_수_없다면_404를_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - - final Long invalidChatRoomId = -999L; - final ChatRoomNotFoundException chatRoomNotFoundException = - new ChatRoomNotFoundException("지정한 아이디에 대한 채팅방을 찾을 수 없습니다."); - - given(chatRoomService.readByChatRoomId(anyLong(), anyLong())).willThrow(chatRoomNotFoundException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(chatRoomService.readByChatRoomId(anyLong(), anyLong())).willThrow(new ChatRoomNotFoundException("지정한 아이디에 대한 채팅방을 찾을 수 없습니다.")); // when & then - mockMvc.perform(get("/chattings/{chatRoomId}", invalidChatRoomId) + mockMvc.perform(get("/chattings/{chatRoomId}", 유효하지_않은_채팅방_아이디) .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") .contentType(MediaType.APPLICATION_JSON)) .andExpectAll( status().isNotFound(), - jsonPath("$.message", is(chatRoomNotFoundException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 지정한_아이디에_해당하는_채팅방_조회시_요청한_사용자_채팅방의_참여자가_아니라면_404를_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - - final UserNotAccessibleException userNotAccessibleException = - new UserNotAccessibleException("해당 채팅방에 접근할 권한이 없습니다."); - - given(chatRoomService.readByChatRoomId(anyLong(), anyLong())).willThrow(userNotAccessibleException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(chatRoomService.readByChatRoomId(anyLong(), anyLong())).willThrow(new InvalidUserToChat("해당 채팅방에 접근할 권한이 없습니다.")); // when & then - mockMvc.perform(get("/chattings/1") + mockMvc.perform(get("/chattings/{chatRoomId}", 채팅방_아이디) .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") .contentType(MediaType.APPLICATION_JSON)) .andExpectAll( status().isForbidden(), - jsonPath("$.message", is(userNotAccessibleException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 채팅방을_생성한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - - final Long newChatRoomId = 1L; - final CreateChatRoomRequest chatRoomRequest = new CreateChatRoomRequest(1L); - - given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willReturn(newChatRoomId); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willReturn(채팅방_아이디); // when & then - mockMvc.perform(post("/chattings") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(chatRoomRequest))) - .andExpectAll( - status().isCreated(), - header().string(HttpHeaders.LOCATION, is("/chattings/" + newChatRoomId)) - ) - .andDo( - restDocs.document( - requestHeaders( - headerWithName("Authorization").description("회원 Bearer 인증 정보") - ), - requestFields( - fieldWithPath("auctionId").type(JsonFieldType.NUMBER).description("연관된 경매 ID") - ), - responseFields( - fieldWithPath("chatRoomId").type(JsonFieldType.NUMBER).description("생성된 채팅방 ID") - ) - ) - ); + final ResultActions resultActions = mockMvc.perform(post("/chattings") + .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(채팅방_생성_요청))) + .andExpectAll( + status().isCreated(), + header().string(HttpHeaders.LOCATION, is("/chattings/" + 채팅방_아이디)) + ); + createChatRoom_문서화(resultActions); } @Test void 채팅방_생성시_요청한_사용자_정보를_찾을_수_없다면_404를_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - - final CreateChatRoomRequest chatRoomRequest = new CreateChatRoomRequest(1L); - final UserNotFoundException userNotFoundException = new UserNotFoundException("사용자 정보를 찾을 수 없습니다."); - - given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willThrow(userNotFoundException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willThrow(new UserNotFoundException("사용자 정보를 찾을 수 없습니다.")); // when & then mockMvc.perform(post("/chattings") .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(chatRoomRequest))) + .content(objectMapper.writeValueAsString(채팅방_생성_요청))) .andExpectAll( status().isNotFound(), - jsonPath("$.message", is(userNotFoundException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 채팅방_생성시_관련된_경매_정보를_찾을_수_없다면_404를_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - - final Long invalidAuctionId = 999L; - final CreateChatRoomRequest chatRoomRequest = new CreateChatRoomRequest(invalidAuctionId); - final AuctionNotFoundException auctionNotFoundException = new AuctionNotFoundException("해당 경매를 찾을 수 없습니다."); - - given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willThrow(auctionNotFoundException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willThrow(new AuctionNotFoundException("해당 경매를 찾을 수 없습니다.")); // when & then mockMvc.perform(post("/chattings") .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(chatRoomRequest))) + .content(objectMapper.writeValueAsString(존재하지_않은_경매_아이디_채팅방_생성_요청))) .andExpectAll( status().isNotFound(), - jsonPath("$.message", is(auctionNotFoundException.getMessage())) + jsonPath("$.message").exists() ); } @Test - void 경매가_종료되지_않은_상태에서_채팅방을_생성하면_400을_반환한다() throws Exception { + void 채팅방_생성시_유효하지_않은_경매_아이디를_전달받는다면_400를_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - - final CreateChatRoomRequest chatRoomRequest = new CreateChatRoomRequest(1L); - final InvalidAuctionToChatException invalidAuctionToChatException = - new InvalidAuctionToChatException("경매가 아직 종료되지 않았습니다."); - - given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willThrow(invalidAuctionToChatException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); // when & then mockMvc.perform(post("/chattings") .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(chatRoomRequest))) + .content(objectMapper.writeValueAsString(유효하지_않은_경매_아이디_채팅방_생성_요청))) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is(invalidAuctionToChatException.getMessage())) + jsonPath("$.message").exists() ); } @Test - void 경매가_삭제된_상태에서_채팅방을_생성하면_400을_반환한다() throws Exception { + void 경매가_종료되지_않은_상태에서_채팅방을_생성하면_400을_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - - final CreateChatRoomRequest chatRoomRequest = new CreateChatRoomRequest(1L); - final InvalidAuctionToChatException invalidAuctionToChatException = - new InvalidAuctionToChatException("삭제된 경매입니다."); - - given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willThrow(invalidAuctionToChatException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willThrow(new InvalidAuctionToChatException("경매가 아직 종료되지 않았습니다.")); // when & then mockMvc.perform(post("/chattings") .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(chatRoomRequest))) + .content(objectMapper.writeValueAsString(채팅방_생성_요청))) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is(invalidAuctionToChatException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 채팅방_생성시_낙찰자가_없다면_404를_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - - final CreateChatRoomRequest chatRoomRequest = new CreateChatRoomRequest(1L); - final WinnerNotFoundException winnerNotFoundException = new WinnerNotFoundException("낙찰자가 존재하지 않습니다"); - - given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willThrow(winnerNotFoundException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willThrow(new WinnerNotFoundException("낙찰자가 존재하지 않습니다")); // when & then mockMvc.perform(post("/chattings") .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(chatRoomRequest))) + .content(objectMapper.writeValueAsString(채팅방_생성_요청))) .andExpectAll( status().isNotFound(), - jsonPath("$.message", is(winnerNotFoundException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 채팅방_생성을_요청한_사용자가_경매의_판매자_또는_최종_낙찰자가_아니라면_403을_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - - final CreateChatRoomRequest chatRoomRequest = new CreateChatRoomRequest(1L); - final UserNotAccessibleException userNotAccessibleException = - new UserNotAccessibleException("경매의 판매자 또는 최종 낙찰자만 채팅이 가능합니다."); - - given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willThrow(userNotAccessibleException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willThrow(new InvalidUserToChat("경매의 판매자 또는 최종 낙찰자만 채팅이 가능합니다.")); // when & then mockMvc.perform(post("/chattings") .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(chatRoomRequest))) + .content(objectMapper.writeValueAsString(채팅방_생성_요청))) .andExpectAll( status().isForbidden(), - jsonPath("$.message", is(userNotAccessibleException.getMessage())) + jsonPath("$.message").exists() ); } + + private void createMessage_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + pathParameters( + parameterWithName("chatRoomId").description("메시지를 보내고 싶은 채팅방의 ID") + ), + requestFields( + fieldWithPath("receiverId").description("메시지 수신자 ID"), + fieldWithPath("contents").description("메시지 내용") + ), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER) + .description("메시지 보내진 채팅방 ID") + ) + ) + ); + } + + private void readAllByLastMessageId_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + pathParameters( + parameterWithName("chatRoomId").description("메시지를 보내고 싶은 채팅방의 ID") + ), + queryParameters( + parameterWithName("lastMessageId").description("마지막으로 응답받은 메시지의 ID").optional() + ), + responseFields( + fieldWithPath("[]").type(JsonFieldType.ARRAY) + .description("하나의 채팅방 내의 메시지 목록 (lastMessageId가 포함되어 있다면 lastMessageId 이후의 메시지 목록"), + fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("메시지 ID"), + fieldWithPath("[].createdAt").type(JsonFieldType.STRING) + .description("메시지를 보낸 시간"), + fieldWithPath("[].isMyMessage").type(JsonFieldType.BOOLEAN) + .description("조회를 요청한 사람이 보낸 메시지인지 여부"), + fieldWithPath("[].contents").type(JsonFieldType.STRING) + .description("메시지 내용") + ) + ) + ); + } + + private void readAllParticipatingChatRooms_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + responseFields( + fieldWithPath("[]").type(JsonFieldType.ARRAY).description("자신이 참여한 채팅방 목록"), + fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("채팅방 ID"), + fieldWithPath("[].chatPartner").type(JsonFieldType.OBJECT).description("채팅 상대방"), + fieldWithPath("[].chatPartner.id").type(JsonFieldType.NUMBER) + .description("채팅 상대방 ID"), + fieldWithPath("[].chatPartner.name").type(JsonFieldType.STRING) + .description("채팅 상대방 이름"), + fieldWithPath("[].chatPartner.profileImage").type(JsonFieldType.STRING) + .description("채팅 상대방 프로필 사진"), + fieldWithPath("[].auction").type(JsonFieldType.OBJECT) + .description("채팅방과 연관된 경매"), + fieldWithPath("[].auction.id").type(JsonFieldType.NUMBER).description("경매 ID"), + fieldWithPath("[].auction.title").type(JsonFieldType.STRING) + .description("경매 제목"), + fieldWithPath("[].auction.image").type(JsonFieldType.STRING) + .description("경매 대표 사진"), + fieldWithPath("[].auction.price").type(JsonFieldType.NUMBER).description("낙찰가"), + fieldWithPath("[].lastMessage").type(JsonFieldType.OBJECT) + .description("마지막으로 전송된 메시지"), + fieldWithPath("[].lastMessage.createdAt").type(JsonFieldType.STRING) + .description("메시지를 보낸 시간"), + fieldWithPath("[].lastMessage.contents").type(JsonFieldType.STRING) + .description("메시지 내용"), + fieldWithPath("[].isChatAvailable").type(JsonFieldType.BOOLEAN) + .description("채팅 가능 여부") + ) + ) + ); + } + + private void readChatRoom_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + pathParameters( + parameterWithName("chatRoomId").description("조회하고자 하는 채팅방 ID") + ), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("채팅방 ID"), + fieldWithPath("auction").type(JsonFieldType.OBJECT).description("채팅방과 연관된 경매"), + fieldWithPath("auction.id").type(JsonFieldType.NUMBER).description("경매 ID"), + fieldWithPath("auction.title").type(JsonFieldType.STRING).description("경매 제목"), + fieldWithPath("auction.image").type(JsonFieldType.STRING) + .description("경매 대표 사진"), + fieldWithPath("auction.price").type(JsonFieldType.NUMBER).description("낙찰가"), + fieldWithPath("chatPartner").type(JsonFieldType.OBJECT).description("채팅 상대방"), + fieldWithPath("chatPartner.id").type(JsonFieldType.NUMBER) + .description("채팅 상대방 ID"), + fieldWithPath("chatPartner.name").type(JsonFieldType.STRING) + .description("채팅 상대방 이름"), + fieldWithPath("chatPartner.profileImage").type(JsonFieldType.STRING) + .description("채팅 상대방 프로필 사진"), + fieldWithPath("isChatAvailable").type(JsonFieldType.BOOLEAN) + .description("채팅 가능 여부") + ) + ) + ); + } + + private void createChatRoom_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + requestFields( + fieldWithPath("auctionId").type(JsonFieldType.NUMBER) + .description("연관된 경매 ID") + ), + responseFields( + fieldWithPath("chatRoomId").type(JsonFieldType.NUMBER) + .description("생성된 채팅방 ID") + ) + ) + ); + } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/fixture/ChatRoomControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/fixture/ChatRoomControllerFixture.java new file mode 100644 index 000000000..8f7b5ddaa --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/fixture/ChatRoomControllerFixture.java @@ -0,0 +1,40 @@ +package com.ddang.ddang.chat.presentation.fixture; + +import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims; +import com.ddang.ddang.chat.application.dto.ReadAuctionInChatRoomDto; +import com.ddang.ddang.chat.application.dto.ReadChatRoomWithLastMessageDto; +import com.ddang.ddang.chat.application.dto.ReadLastMessageDto; +import com.ddang.ddang.chat.application.dto.ReadMessageDto; +import com.ddang.ddang.chat.application.dto.ReadParticipatingChatRoomDto; +import com.ddang.ddang.chat.application.dto.ReadUserInChatRoomDto; +import com.ddang.ddang.chat.presentation.dto.request.CreateChatRoomRequest; +import com.ddang.ddang.chat.presentation.dto.request.CreateMessageRequest; +import com.ddang.ddang.configuration.CommonControllerSliceTest; + +import java.time.LocalDateTime; + +@SuppressWarnings("NonAsciiCharacters") +public class ChatRoomControllerFixture extends CommonControllerSliceTest { + + private Long 탈퇴한_사용자_아이디 = 5L; + protected PrivateClaims 사용자_ID_클레임 = new PrivateClaims(1L); + protected ReadUserInChatRoomDto 판매자 = new ReadUserInChatRoomDto(1L, "판매자", 1L, 5.0d, false); + private ReadUserInChatRoomDto 구매자1 = new ReadUserInChatRoomDto(2L, "구매자1", 2L, 5.0d, false); + private ReadUserInChatRoomDto 구매자2 = new ReadUserInChatRoomDto(3L, "구매자2", 3L, 5.0d, false); + private ReadAuctionInChatRoomDto 조회용_경매1 = new ReadAuctionInChatRoomDto(1L, "경매1", 10_000, 1L); + private ReadAuctionInChatRoomDto 조회용_경매2 = new ReadAuctionInChatRoomDto(2L, "경매2", 20_000, 1L); + protected ReadChatRoomWithLastMessageDto 조회용_채팅방1 = new ReadChatRoomWithLastMessageDto(1L, 조회용_경매1, 구매자1, new ReadLastMessageDto(1L, LocalDateTime.now(), 판매자, 구매자1, "메시지1"), true); + protected ReadChatRoomWithLastMessageDto 조회용_채팅방2 = new ReadChatRoomWithLastMessageDto(2L, 조회용_경매2, 구매자2, new ReadLastMessageDto(1L, LocalDateTime.now(), 판매자, 구매자2, "메시지2"), true); + protected CreateMessageRequest 메시지_생성_요청 = new CreateMessageRequest(1L, "메시지 내용"); + protected CreateMessageRequest 유효하지_않은_발신자의_메시지_생성_요청 = new CreateMessageRequest(-999L, "메시지 내용"); + protected CreateMessageRequest 탈퇴한_사용자와의_메시지_생성_요청 = new CreateMessageRequest(탈퇴한_사용자_아이디, "메시지 내용"); + protected ReadMessageDto 조회용_메시지 = new ReadMessageDto(1L, LocalDateTime.now(), 1L, 1L, 1L, "메시지내용"); + protected ReadParticipatingChatRoomDto 조회용_참가중인_채팅방 = new ReadParticipatingChatRoomDto(1L, 조회용_경매1, 판매자, true); + protected CreateChatRoomRequest 채팅방_생성_요청 = new CreateChatRoomRequest(1L); + protected CreateChatRoomRequest 존재하지_않은_경매_아이디_채팅방_생성_요청 = new CreateChatRoomRequest(1L); + protected CreateChatRoomRequest 유효하지_않은_경매_아이디_채팅방_생성_요청 = new CreateChatRoomRequest(-999L); + protected Long 채팅방_아이디 = 1L; + protected Long 유효하지_않은_채팅방_아이디 = -999L; + protected Long 마지막_메시지_아이디 = 1L; + protected Long 유효하지_않은_마지막_메시지_아이디 = -999L; +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/configuration/CommonControllerSliceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/configuration/CommonControllerSliceTest.java new file mode 100644 index 000000000..cb8a9617c --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/configuration/CommonControllerSliceTest.java @@ -0,0 +1,189 @@ +package com.ddang.ddang.configuration; + +import com.ddang.ddang.auction.application.AuctionService; +import com.ddang.ddang.auction.presentation.AuctionController; +import com.ddang.ddang.auction.presentation.AuctionQnaController; +import com.ddang.ddang.auction.presentation.AuctionReviewController; +import com.ddang.ddang.authentication.application.AuthenticationService; +import com.ddang.ddang.authentication.application.AuthenticationUserService; +import com.ddang.ddang.authentication.application.BlackListTokenService; +import com.ddang.ddang.authentication.presentation.AuthenticationController; +import com.ddang.ddang.bid.application.BidService; +import com.ddang.ddang.bid.presentation.BidController; +import com.ddang.ddang.category.application.CategoryService; +import com.ddang.ddang.category.presentation.CategoryController; +import com.ddang.ddang.chat.application.ChatRoomService; +import com.ddang.ddang.chat.application.MessageService; +import com.ddang.ddang.chat.presentation.ChatRoomController; +import com.ddang.ddang.device.application.DeviceTokenService; +import com.ddang.ddang.device.presentation.DeviceTokenController; +import com.ddang.ddang.image.application.ImageService; +import com.ddang.ddang.image.presentation.ImageController; +import com.ddang.ddang.qna.application.AnswerService; +import com.ddang.ddang.qna.application.QuestionService; +import com.ddang.ddang.qna.presentation.QnaController; +import com.ddang.ddang.region.application.RegionService; +import com.ddang.ddang.region.presentation.RegionController; +import com.ddang.ddang.report.application.AnswerReportService; +import com.ddang.ddang.report.application.AuctionReportService; +import com.ddang.ddang.report.application.ChatRoomReportService; +import com.ddang.ddang.report.application.QuestionReportService; +import com.ddang.ddang.report.presentation.ReportController; +import com.ddang.ddang.review.application.ReviewService; +import com.ddang.ddang.review.presentation.ReviewController; +import com.ddang.ddang.user.application.UserService; +import com.ddang.ddang.user.presentation.UserAuctionController; +import com.ddang.ddang.user.presentation.UserController; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.Import; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@WebMvcTest( + controllers = { + AuctionController.class, + AuctionQnaController.class, + AuthenticationController.class, + BidController.class, + CategoryController.class, + ChatRoomController.class, + DeviceTokenController.class, + ImageController.class, + RegionController.class, + ReportController.class, + UserAuctionController.class, + UserController.class, + QnaController.class, + ReviewController.class, + AuctionReviewController.class + }, + excludeFilters = { + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfigurer.class), + @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com\\.ddang\\.ddang\\.authentication\\.configuration\\..*") + } +) +@AutoConfigureRestDocs +@Import(RestDocsConfiguration.class) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +public abstract class CommonControllerSliceTest { + + @Autowired + protected ObjectMapper objectMapper; + + @Autowired + protected RestDocumentationResultHandler restDocs; + + @Autowired + protected RestDocumentationContextProvider provider; + + @Autowired + protected AuctionController auctionController; + + @Autowired + protected AuctionQnaController auctionQnaController; + + @Autowired + protected AuthenticationController authenticationController; + + @Autowired + protected CategoryController categoryController; + + @Autowired + protected BidController bidController; + + @Autowired + protected ChatRoomController chatRoomController; + + @Autowired + protected DeviceTokenController deviceTokenController; + + @Autowired + protected ImageController imageController; + + @Autowired + protected RegionController regionController; + + @Autowired + protected ReportController reportController; + + @Autowired + protected UserAuctionController userAuctionController; + + @Autowired + protected UserController userController; + + @Autowired + protected QnaController qnaController; + + @Autowired + protected ReviewController reviewController; + + @Autowired + protected AuctionReviewController auctionReviewController; + + @MockBean + protected AuctionService auctionService; + + @MockBean + protected ChatRoomService chatRoomService; + + @MockBean + protected BlackListTokenService blackListTokenService; + + @MockBean + protected AuthenticationUserService authenticationUserService; + + @MockBean + protected AuthenticationService authenticationService; + + @MockBean + protected BidService bidService; + + @MockBean + protected CategoryService categoryService; + + @MockBean + protected MessageService messageService; + + @MockBean + protected DeviceTokenService deviceTokenService; + + @MockBean + protected ImageService imageService; + + @MockBean + protected RegionService regionService; + + @MockBean + protected AuctionReportService auctionReportService; + + @MockBean + protected ChatRoomReportService chatRoomReportService; + + @MockBean + protected QuestionReportService questionReportService; + + @MockBean + protected AnswerReportService answerReportService; + + @MockBean + protected UserService userService; + + @MockBean + protected QuestionService questionService; + + @MockBean + protected AnswerService answerService; + + @MockBean + protected ReviewService reviewService; +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/configuration/FcmConfiguration.java b/backend/ddang/src/test/java/com/ddang/ddang/configuration/FcmConfiguration.java new file mode 100644 index 000000000..ab5f8ec2e --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/configuration/FcmConfiguration.java @@ -0,0 +1,41 @@ +package com.ddang.ddang.configuration; + +import com.ddang.ddang.configuration.fcm.MockGoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import java.util.List; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +@Configuration +@Profile("test") +public class FcmConfiguration { + + @Bean + public FirebaseMessaging firebaseMessaging() { + final FirebaseApp firebaseApps = findFirebaseApps(); + + return FirebaseMessaging.getInstance(firebaseApps); + } + + private FirebaseApp findFirebaseApps() { + final List apps = FirebaseApp.getApps(); + + if (!apps.isEmpty()) { + for (final FirebaseApp app : apps) { + if (FirebaseApp.DEFAULT_APP_NAME.equals(app.getName())) { + return app; + } + } + } + + final FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + + return FirebaseApp.initializeApp(options); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/device/application/DeviceTokenServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/device/application/DeviceTokenServiceTest.java new file mode 100644 index 000000000..8d3b0e484 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/device/application/DeviceTokenServiceTest.java @@ -0,0 +1,69 @@ +package com.ddang.ddang.device.application; + +import com.ddang.ddang.configuration.IsolateDatabase; +import com.ddang.ddang.device.application.fixture.DeviceTokenServiceFixture; +import com.ddang.ddang.device.domain.DeviceToken; +import com.ddang.ddang.device.infrastructure.persistence.JpaDeviceTokenRepository; +import com.ddang.ddang.user.application.exception.UserNotFoundException; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@IsolateDatabase +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class DeviceTokenServiceTest extends DeviceTokenServiceFixture { + + @Autowired + DeviceTokenService deviceTokenService; + + @Autowired + JpaDeviceTokenRepository deviceTokenRepository; + + @Test + void 사용자의_디바이스_토큰이_존재하지_않는다면_저장한다() { + // when & then + assertThatNoException().isThrownBy( + () -> deviceTokenService.persist(디바이스_토큰이_없는_사용자.getId(), 디바이스_토큰_저장을_위한_DTO) + ); + } + + @Test + void 사용자의_디바이스_토큰이_이미_존재하고_새로운_토큰이_주어진다면_토큰을_갱신한다() { + // when + deviceTokenService.persist(디바이스_토큰이_있는_사용자.getId(), 디바이스_토큰_갱신을_위한_DTO); + + // then + final Optional deviceTokenResult = deviceTokenRepository.findByUserId(디바이스_토큰이_있는_사용자.getId()); + final String actual = deviceTokenResult.get().getDeviceToken(); + + assertThat(actual).isEqualTo(갱신된_디바이스_토큰_값); + } + + @Test + void 사용자의_디바이스_토큰이_이미_존재하고_동일한_토큰이_주어진다면_토큰을_갱신하지_않는다() { + // when + deviceTokenService.persist(디바이스_토큰이_있는_사용자.getId(), 존재하는_디바이스_토큰과_동일한_토큰을_저장하려는_DTO); + + // then + final Optional userDeviceToken = deviceTokenRepository.findByUserId(디바이스_토큰이_있는_사용자.getId()); + final String actual = userDeviceToken.get().getDeviceToken(); + + assertThat(actual).isEqualTo(사용_중인_디바이스_토큰_값); + } + + @Test + void 사용자를_찾을_수_없다면_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> deviceTokenService.persist(존재하지_않는_사용자_아이디, 디바이스_토큰_저장을_위한_DTO)) + .isInstanceOf(UserNotFoundException.class) + .hasMessage("해당 사용자를 찾을 수 없습니다."); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/device/application/fixture/DeviceTokenServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/device/application/fixture/DeviceTokenServiceFixture.java new file mode 100644 index 000000000..0006789d2 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/device/application/fixture/DeviceTokenServiceFixture.java @@ -0,0 +1,57 @@ +package com.ddang.ddang.device.application.fixture; + +import com.ddang.ddang.device.application.dto.PersistDeviceTokenDto; +import com.ddang.ddang.device.domain.DeviceToken; +import com.ddang.ddang.device.infrastructure.persistence.JpaDeviceTokenRepository; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class DeviceTokenServiceFixture { + + @Autowired + private JpaDeviceTokenRepository deviceTokenRepository; + + @Autowired + private JpaUserRepository userRepository; + private String 초기_디바이스_토큰_값 = "initialDeviceToken"; + private DeviceToken 사용자의_디바이스_토큰; + + protected String 사용_중인_디바이스_토큰_값 = "usingDeviceToken"; + protected String 갱신된_디바이스_토큰_값 = "newDeviceToken"; + protected Long 존재하지_않는_사용자_아이디 = -999L; + protected User 디바이스_토큰이_있는_사용자; + protected User 디바이스_토큰이_없는_사용자; + protected PersistDeviceTokenDto 디바이스_토큰_저장을_위한_DTO; + protected PersistDeviceTokenDto 디바이스_토큰_갱신을_위한_DTO; + protected PersistDeviceTokenDto 존재하는_디바이스_토큰과_동일한_토큰을_저장하려는_DTO; + + @BeforeEach + void setUp() { + 디바이스_토큰이_있는_사용자 = User.builder() + .name("디바이스 토큰이 있는 사용자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + 디바이스_토큰이_없는_사용자 = User.builder() + .name("디바이스 토큰이 없는 사용자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + 사용자의_디바이스_토큰 = new DeviceToken(디바이스_토큰이_있는_사용자, 사용_중인_디바이스_토큰_값); + userRepository.saveAll(List.of(디바이스_토큰이_있는_사용자, 디바이스_토큰이_없는_사용자)); + deviceTokenRepository.save(사용자의_디바이스_토큰); + + 디바이스_토큰_저장을_위한_DTO = new PersistDeviceTokenDto(초기_디바이스_토큰_값); + 디바이스_토큰_갱신을_위한_DTO = new PersistDeviceTokenDto(갱신된_디바이스_토큰_값); + 존재하는_디바이스_토큰과_동일한_토큰을_저장하려는_DTO = new PersistDeviceTokenDto(사용_중인_디바이스_토큰_값); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/device/domain/DeviceTokenTest.java b/backend/ddang/src/test/java/com/ddang/ddang/device/domain/DeviceTokenTest.java new file mode 100644 index 000000000..4fb5979f4 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/device/domain/DeviceTokenTest.java @@ -0,0 +1,47 @@ +package com.ddang.ddang.device.domain; + +import com.ddang.ddang.device.domain.fixture.DeviceTokenFixture; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("NonAsciiCharacters") +class DeviceTokenTest extends DeviceTokenFixture { + + + @Test + void 디바이스_토큰이_다르다면_참을_반환한다() { + // given + final DeviceToken deviceToken = new DeviceToken(사용자, 디바이스_토큰); + + // when + final boolean actual = deviceToken.isDifferentToken(새로운_디바이스_토큰); + + // then + assertThat(actual).isTrue(); + } + + @Test + void 디바이스_토큰이_같다면_거짓을_반환한다() { + // given + final DeviceToken deviceToken = new DeviceToken(사용자, 디바이스_토큰); + + // when + final boolean actual = deviceToken.isDifferentToken(디바이스_토큰); + + // then + assertThat(actual).isFalse(); + } + + @Test + void 디바이스_토큰을_갱신한다() { + // given + final DeviceToken deviceToken = new DeviceToken(사용자, 디바이스_토큰); + + // when + deviceToken.updateDeviceToken(새로운_디바이스_토큰); + + // then + assertThat(deviceToken.getDeviceToken()).isEqualTo(새로운_디바이스_토큰); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/device/domain/fixture/DeviceTokenFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/device/domain/fixture/DeviceTokenFixture.java new file mode 100644 index 000000000..b16956623 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/device/domain/fixture/DeviceTokenFixture.java @@ -0,0 +1,24 @@ +package com.ddang.ddang.device.domain.fixture; + +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import org.junit.jupiter.api.BeforeEach; + +@SuppressWarnings("NonAsciiCharacters") +public class DeviceTokenFixture { + + protected String 디바이스_토큰 = "deviceToken"; + protected String 새로운_디바이스_토큰 = "newDeviceToken"; + protected User 사용자; + + @BeforeEach + void setUp() { + 사용자 = User.builder() + .name("사용자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/device/infrastructure/persistence/JpaDeviceTokenRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/device/infrastructure/persistence/JpaDeviceTokenRepositoryTest.java new file mode 100644 index 000000000..06dbc3a7a --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/device/infrastructure/persistence/JpaDeviceTokenRepositoryTest.java @@ -0,0 +1,35 @@ +package com.ddang.ddang.device.infrastructure.persistence; + +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.ddang.ddang.device.domain.DeviceToken; +import com.ddang.ddang.device.infrastructure.persistence.fixture.JpaDeviceTokenRepositoryFixture; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class JpaDeviceTokenRepositoryTest extends JpaDeviceTokenRepositoryFixture { + + @Autowired + JpaDeviceTokenRepository userDeviceTokenRepository; + + @Test + void 주어진_사용자_아이디에_해당하는_기기토큰을_조회한다() { + // when + final Optional actual = userDeviceTokenRepository.findByUserId(사용자.getId()); + + // then + assertThat(actual).contains(사용자의_디바이스_토큰); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/device/infrastructure/persistence/fixture/JpaDeviceTokenRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/device/infrastructure/persistence/fixture/JpaDeviceTokenRepositoryFixture.java new file mode 100644 index 000000000..128847999 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/device/infrastructure/persistence/fixture/JpaDeviceTokenRepositoryFixture.java @@ -0,0 +1,46 @@ +package com.ddang.ddang.device.infrastructure.persistence.fixture; + +import com.ddang.ddang.device.domain.DeviceToken; +import com.ddang.ddang.device.infrastructure.persistence.JpaDeviceTokenRepository; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +@SuppressWarnings("NonAsciiCharacters") +public class JpaDeviceTokenRepositoryFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaDeviceTokenRepository deviceTokenRepository; + + protected User 사용자; + protected DeviceToken 사용자의_디바이스_토큰; + + @BeforeEach + void setUp() { + 사용자 = User.builder() + .name("사용자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + + 사용자의_디바이스_토큰 = new DeviceToken(사용자, "deviceToken"); + + userRepository.save(사용자); + deviceTokenRepository.save(사용자의_디바이스_토큰); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/device/presentation/DeviceTokenControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/device/presentation/DeviceTokenControllerTest.java new file mode 100644 index 000000000..c1fe19399 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/device/presentation/DeviceTokenControllerTest.java @@ -0,0 +1,119 @@ +package com.ddang.ddang.device.presentation; + +import com.ddang.ddang.auction.configuration.DescendingSortPageableArgumentResolver; +import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; +import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; +import com.ddang.ddang.authentication.domain.TokenDecoder; +import com.ddang.ddang.authentication.domain.TokenType; +import com.ddang.ddang.authentication.domain.dto.AuthenticationStore; +import com.ddang.ddang.device.application.dto.PersistDeviceTokenDto; +import com.ddang.ddang.device.presentation.fixture.DeviceTokenControllerFixture; +import com.ddang.ddang.exception.GlobalExceptionHandler; +import com.ddang.ddang.user.application.exception.UserNotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SuppressWarnings("NonAsciiCharacters") +class DeviceTokenControllerTest extends DeviceTokenControllerFixture { + + TokenDecoder tokenDecoder; + MockMvc mockMvc; + + @BeforeEach + void setUp() { + tokenDecoder = mock(TokenDecoder.class); + + final AuthenticationStore store = new AuthenticationStore(); + final AuthenticationInterceptor interceptor = new AuthenticationInterceptor( + blackListTokenService, + authenticationUserService, + tokenDecoder, + store + ); + final AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(store); + + mockMvc = MockMvcBuilders.standaloneSetup(deviceTokenController) + .setControllerAdvice(new GlobalExceptionHandler()) + .addInterceptors(interceptor) + .setCustomArgumentResolvers(resolver, new DescendingSortPageableArgumentResolver()) + .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) + .alwaysDo(print()) + .alwaysDo(restDocs) + .build(); + } + + @Test + void 디바이스_토큰을_저장_또는_갱신한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(유효한_사용자_ID_클레임)); + doNothing().when(deviceTokenService).persist(anyLong(), any(PersistDeviceTokenDto.class)); + + // when & then + final ResultActions resultActions = + mockMvc.perform(patch("/device-token") + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + .content(objectMapper.writeValueAsString(디바이스_토큰_갱신_요청))) + .andExpectAll( + status().isOk() + ); + + update_문서화(resultActions); + } + + @Test + void 사용자를_찾을_수_없는_경우_404를_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(유효하지_않은_사용자_ID_클레임)); + willThrow(new UserNotFoundException("해당 사용자를 찾을 수 없습니다.")) + .given(deviceTokenService).persist(anyLong(), any(PersistDeviceTokenDto.class)); + + // when & then + mockMvc.perform(patch("/device-token") + .header(HttpHeaders.AUTHORIZATION, 유효하지_않은_액세스_토큰_값) + .content(objectMapper.writeValueAsString(디바이스_토큰_갱신_요청)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpectAll( + status().isNotFound(), + jsonPath("$.message").exists() + ); + } + + private void update_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + requestFields( + fieldWithPath("deviceToken").description("디바이스 토큰") + ) + ) + ); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/device/presentation/fixture/DeviceTokenControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/device/presentation/fixture/DeviceTokenControllerFixture.java new file mode 100644 index 000000000..ae1522eeb --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/device/presentation/fixture/DeviceTokenControllerFixture.java @@ -0,0 +1,15 @@ +package com.ddang.ddang.device.presentation.fixture; + +import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims; +import com.ddang.ddang.configuration.CommonControllerSliceTest; +import com.ddang.ddang.device.presentation.dto.request.UpdateDeviceTokenRequest; + +@SuppressWarnings("NonAsciiCharacters") +public class DeviceTokenControllerFixture extends CommonControllerSliceTest { + + protected String 액세스_토큰_값 = "Bearer accessToken"; + protected String 유효하지_않은_액세스_토큰_값 = "Bearer invalidAccessToken"; + protected PrivateClaims 유효한_사용자_ID_클레임 = new PrivateClaims(1L); + protected PrivateClaims 유효하지_않은_사용자_ID_클레임 = new PrivateClaims(-999L); + protected UpdateDeviceTokenRequest 디바이스_토큰_갱신_요청 = new UpdateDeviceTokenRequest("newDeviceToken"); +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/application/ImageServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/image/application/ImageServiceTest.java index bb13f02ff..e2044c85c 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/image/application/ImageServiceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/application/ImageServiceTest.java @@ -1,54 +1,59 @@ package com.ddang.ddang.image.application; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.ddang.ddang.image.application.exception.ImageNotFoundException; -import com.ddang.ddang.image.domain.AuctionImage; -import com.ddang.ddang.image.infrastructure.persistence.JpaAuctionImageRepository; +import com.ddang.ddang.configuration.IsolateDatabase; +import com.ddang.ddang.image.application.fixture.ImageServiceFixture; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.core.io.Resource; -import org.springframework.transaction.annotation.Transactional; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@Transactional +import java.net.MalformedURLException; + +import static org.assertj.core.api.Assertions.assertThat; + +@IsolateDatabase @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class ImageServiceTest { +class ImageServiceTest extends ImageServiceFixture { @Autowired ImageService imageService; - @Autowired - JpaAuctionImageRepository auctionImageRepository; + @Test + void 지정한_아이디에_해당하는_프로필_이미지를_조회한다() throws Exception { + // when + final Resource actual = imageService.readProfileImage(프로필_이미지.getId()); + + // then + assertThat(actual.getFilename()).isEqualTo(프로필_이미지_파일명); + } @Test - void 지정한_아이디에_해당하는_경매_이미지를_조회한다() throws Exception { - // given - final AuctionImage auctionImage = new AuctionImage("image.png", "image.png"); + void 지정한_아이디에_해당하는_프로필_이미지가_없는_경우_null을_반환한다() throws MalformedURLException { + // when + final Resource actual = imageService.readProfileImage(존재하지_않는_프로필_이미지_아이디); - auctionImageRepository.save(auctionImage); + // then + assertThat(actual).isNull(); + } + @Test + void 지정한_아이디에_해당하는_경매_이미지를_조회한다() throws Exception { // when - final Resource actual = imageService.readAuctionImage(auctionImage.getId()); + final Resource actual = imageService.readAuctionImage(경매_이미지.getId()); // then - assertThat(actual.getFilename()).isEqualTo("image.png"); + assertThat(actual.getFilename()).isEqualTo(경매_이미지_파일명); } @Test - void 지정한_아이디에_해당하는_경매_이미지가_없는_경우_예외가_발생한다() { - // given - final Long invalidAuctionImageId = -999L; - - // when & then - assertThatThrownBy(() -> imageService.readAuctionImage(invalidAuctionImageId)) - .isInstanceOf(ImageNotFoundException.class) - .hasMessage("지정한 이미지를 찾을 수 없습니다."); + void 지정한_아이디에_해당하는_경매_이미지가_없는_경우_null을_반환한다() throws MalformedURLException { + // when + final Resource actual = imageService.readAuctionImage(존재하지_않는_경매_이미지_아이디); + + // then + assertThat(actual).isNull(); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/application/fixture/ImageServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/image/application/fixture/ImageServiceFixture.java new file mode 100644 index 000000000..c70725205 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/application/fixture/ImageServiceFixture.java @@ -0,0 +1,39 @@ +package com.ddang.ddang.image.application.fixture; + +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.image.infrastructure.persistence.JpaAuctionImageRepository; +import com.ddang.ddang.image.infrastructure.persistence.JpaProfileImageRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +@SuppressWarnings("NonAsciiCharacters") +public class ImageServiceFixture { + + @Autowired + private JpaProfileImageRepository profileImageRepository; + + @Autowired + private JpaAuctionImageRepository auctionImageRepository; + + protected Long 존재하지_않는_프로필_이미지_아이디 = -999L; + protected Long 존재하지_않는_경매_이미지_아이디 = -999L; + + protected ProfileImage 프로필_이미지; + protected String 프로필_이미지_파일명; + + protected AuctionImage 경매_이미지; + protected String 경매_이미지_파일명; + + @BeforeEach + void setUp() { + 프로필_이미지 = new ProfileImage("upload.png", "store.png"); + 프로필_이미지_파일명 = 프로필_이미지.getImage().getStoreName(); + + 경매_이미지 = new AuctionImage("upload.png", "store.png"); + 경매_이미지_파일명 = 경매_이미지.getImage().getStoreName(); + + profileImageRepository.save(프로필_이미지); + auctionImageRepository.save(경매_이미지); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/application/util/ImageIdProcessorTest.java b/backend/ddang/src/test/java/com/ddang/ddang/image/application/util/ImageIdProcessorTest.java new file mode 100644 index 000000000..a016206cd --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/application/util/ImageIdProcessorTest.java @@ -0,0 +1,56 @@ +package com.ddang.ddang.image.application.util; + +import com.ddang.ddang.image.application.util.fixture.ImageIdProcessorFixture; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class ImageIdProcessorTest extends ImageIdProcessorFixture { + + @Test + void 경매_이미지가_null이_아니라면_경매_이미지_아이디를_반환한다() { + // given + given(경매_이미지.getId()).willReturn(경매_이미지_아이디); + + // when + final Long actual = ImageIdProcessor.process(경매_이미지); + + // then + assertThat(actual).isEqualTo(경매_이미지_아이디); + } + + @Test + void 경매_이미지가_null이면_경매_이미지_아이디를_null로_반환한다() { + // when + final Long actual = ImageIdProcessor.process(null인_경매_이미지); + + // then + assertThat(actual).isNull(); + } + + @Test + void 프로필_이미지가_null이_아니라면_프로필_이미지_아이디를_반환한다() { + // given + given(프로필_이미지.getId()).willReturn(프로필_이미지_아이디); + + // when + final Long actual = ImageIdProcessor.process(프로필_이미지); + + // then + assertThat(actual).isEqualTo(프로필_이미지_아이디); + } + + @Test + void 프로필_이미지가_null이면_프로필_이미지_아이디를_null로_반환한다() { + // when + final Long actual = ImageIdProcessor.process(null인_프로필_이미지); + + // then + assertThat(actual).isNull(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/application/util/fixture/ImageIdProcessorFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/image/application/util/fixture/ImageIdProcessorFixture.java new file mode 100644 index 000000000..3fa95dbd4 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/application/util/fixture/ImageIdProcessorFixture.java @@ -0,0 +1,18 @@ +package com.ddang.ddang.image.application.util.fixture; + +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; + +import static org.mockito.Mockito.mock; + +@SuppressWarnings("NonAsciiCharacters") +public class ImageIdProcessorFixture { + + protected AuctionImage 경매_이미지 = mock(AuctionImage.class); + protected AuctionImage null인_경매_이미지 = null; + protected Long 경매_이미지_아이디 = 1L; + + protected ProfileImage 프로필_이미지 = mock(ProfileImage.class); + protected ProfileImage null인_프로필_이미지 = null; + protected Long 프로필_이미지_아이디 = 1L; +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/domain/AuctionImageTest.java b/backend/ddang/src/test/java/com/ddang/ddang/image/domain/AuctionImageTest.java index 2f36aa0eb..4fe14c1ee 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/image/domain/AuctionImageTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/domain/AuctionImageTest.java @@ -1,26 +1,23 @@ package com.ddang.ddang.image.domain; -import static org.assertj.core.api.Assertions.assertThat; - -import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.image.domain.fixture.AuctionImageFixture; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -@SuppressWarnings("NonAsciiCharacters") +import static org.assertj.core.api.Assertions.assertThat; + @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class AuctionImageTest { +@SuppressWarnings("NonAsciiCharacters") +class AuctionImageTest extends AuctionImageFixture { @Test void 경매_연관_관계를_세팅한다() { // given final AuctionImage auctionImage = new AuctionImage("image.png", "image.png"); - final Auction auction = Auction.builder() - .title("title") - .build(); // when - auctionImage.initAuction(auction); + auctionImage.initAuction(경매); // then assertThat(auctionImage.getAuction()).isNotNull(); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/domain/fixture/AuctionImageFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/image/domain/fixture/AuctionImageFixture.java new file mode 100644 index 000000000..5618167e0 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/domain/fixture/AuctionImageFixture.java @@ -0,0 +1,19 @@ +package com.ddang.ddang.image.domain.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; + +import java.time.LocalDateTime; + +@SuppressWarnings("NonAsciiCharacters") +public class AuctionImageFixture { + + protected Auction 경매 = Auction.builder() + .title("경매 상품") + .description("이것은 경매 상품입니다.") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/local/LocalStoreImageProcessorTest.java b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/local/LocalStoreImageProcessorTest.java index 60ba4f64b..c4bd3c2d6 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/local/LocalStoreImageProcessorTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/local/LocalStoreImageProcessorTest.java @@ -1,39 +1,35 @@ package com.ddang.ddang.image.infrastructure.local; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willDoNothing; -import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.mock; - import com.ddang.ddang.image.domain.dto.StoreImageDto; import com.ddang.ddang.image.infrastructure.local.exception.EmptyImageException; import com.ddang.ddang.image.infrastructure.local.exception.StoreImageFailureException; import com.ddang.ddang.image.infrastructure.local.exception.UnsupportedImageFileExtensionException; -import java.io.File; -import java.io.IOException; -import java.util.List; +import com.ddang.ddang.image.infrastructure.local.fixture.LocalStoreImageProcessorFixture; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.web.multipart.MultipartFile; -@SuppressWarnings("NonAsciiCharacters") +import java.io.File; +import java.io.IOException; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.BDDMockito.willThrow; + @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class LocalStoreImageProcessorTest { +@SuppressWarnings("NonAsciiCharacters") +class LocalStoreImageProcessorTest extends LocalStoreImageProcessorFixture { LocalStoreImageProcessor imageProcessor = new LocalStoreImageProcessor(); @Test void 이미지_파일이_비어_있는_경우_예외가_발생한다() { - // given - final MockMultipartFile imageFile = new MockMultipartFile("image.png", new byte[0]); - // when & then - assertThatThrownBy(() -> imageProcessor.storeImageFiles(List.of(imageFile))) + assertThatThrownBy(() -> imageProcessor.storeImageFiles(List.of(빈_이미지_파일))) .isInstanceOf(EmptyImageException.class) .hasMessage("이미지 파일의 데이터가 비어 있습니다."); } @@ -41,13 +37,11 @@ class LocalStoreImageProcessorTest { @Test void 이미지_저장에_실패한_경우_예외가_발생한다() throws Exception { // given - final MultipartFile imageFile = mock(MultipartFile.class); - - given(imageFile.getOriginalFilename()).willReturn("image.png"); - willThrow(IOException.class).given(imageFile).transferTo(any(File.class)); + given(이미지_파일.getOriginalFilename()).willReturn(기존_이미지_파일명); + willThrow(IOException.class).given(이미지_파일).transferTo(any(File.class)); // when & then - assertThatThrownBy(() -> imageProcessor.storeImageFiles(List.of(imageFile))) + assertThatThrownBy(() -> imageProcessor.storeImageFiles(List.of(이미지_파일))) .isInstanceOf(StoreImageFailureException.class) .hasMessage("이미지 저장에 실패했습니다."); } @@ -55,12 +49,10 @@ class LocalStoreImageProcessorTest { @Test void 허용되지_않은_확장자의_이미지_파일인_경우_예외가_발생한다() { // given - final MultipartFile imageFile = mock(MultipartFile.class); - - given(imageFile.getOriginalFilename()).willReturn("image.gif"); + given(이미지_파일.getOriginalFilename()).willReturn(지원하지_않는_확장자를_가진_이미지_파일명); // when & then - assertThatThrownBy(() -> imageProcessor.storeImageFiles(List.of(imageFile))) + assertThatThrownBy(() -> imageProcessor.storeImageFiles(List.of(이미지_파일))) .isInstanceOf(UnsupportedImageFileExtensionException.class) .hasMessageContaining("지원하지 않는 확장자입니다."); } @@ -68,20 +60,17 @@ class LocalStoreImageProcessorTest { @Test void 유효한_이미지_파일인_경우_이미지_파일을_저장한다() throws Exception { // given - final MultipartFile imageFile = mock(MultipartFile.class); - final String imageFileName = "image.png"; - - given(imageFile.getOriginalFilename()).willReturn(imageFileName); - willDoNothing().given(imageFile).transferTo(any(File.class)); + given(이미지_파일.getOriginalFilename()).willReturn(기존_이미지_파일명); + willDoNothing().given(이미지_파일).transferTo(any(File.class)); // when - final List actual = imageProcessor.storeImageFiles(List.of(imageFile)); + final List actual = imageProcessor.storeImageFiles(List.of(이미지_파일)); // then SoftAssertions.assertSoftly(softAssertions -> { softAssertions.assertThat(actual).hasSize(1); softAssertions.assertThat(actual.get(0).storeName()).isNotBlank(); - softAssertions.assertThat(actual.get(0).uploadName()).isEqualTo(imageFileName); + softAssertions.assertThat(actual.get(0).uploadName()).isEqualTo(기존_이미지_파일명); }); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/local/fixture/LocalStoreImageProcessorFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/local/fixture/LocalStoreImageProcessorFixture.java new file mode 100644 index 000000000..47009aa5f --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/local/fixture/LocalStoreImageProcessorFixture.java @@ -0,0 +1,16 @@ +package com.ddang.ddang.image.infrastructure.local.fixture; + +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import static org.mockito.Mockito.mock; + +@SuppressWarnings("NonAsciiCharacters") +public class LocalStoreImageProcessorFixture { + + protected MockMultipartFile 빈_이미지_파일 = new MockMultipartFile("image.png", new byte[0]); + protected MultipartFile 이미지_파일 = mock(MultipartFile.class); + protected String 기존_이미지_파일명 = "image.png"; + protected String 지원하지_않는_확장자를_가진_이미지_파일명 = "image.gif"; + +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/JpaAuctionImageRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/JpaAuctionImageRepositoryTest.java index 59f4d4dd4..3689fd8be 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/JpaAuctionImageRepositoryTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/JpaAuctionImageRepositoryTest.java @@ -1,13 +1,10 @@ package com.ddang.ddang.image.infrastructure.persistence; -import static org.assertj.core.api.Assertions.assertThat; - import com.ddang.ddang.configuration.QuerydslConfiguration; import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.infrastructure.persistence.fixture.JpaAuctionImageRepositoryFixture; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; -import java.util.Optional; -import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -15,11 +12,15 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + @DataJpaTest +@Import(QuerydslConfiguration.class) @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -@Import(QuerydslConfiguration.class) -class JpaAuctionImageRepositoryTest { +class JpaAuctionImageRepositoryTest extends JpaAuctionImageRepositoryFixture { @PersistenceContext EntityManager em; @@ -28,32 +29,33 @@ class JpaAuctionImageRepositoryTest { JpaAuctionImageRepository auctionImageRepository; @Test - void 지정한_아이디에_해당하는_경매_이미지를_조회한다() { + void 경매_이미지를_저장한다() { // given - final AuctionImage auctionImage = new AuctionImage("uploadName", "storeName"); + final AuctionImage auctionImage = new AuctionImage(업로드_이미지_파일명, 저장된_이미지_파일명); - auctionImageRepository.save(auctionImage); + // when + final AuctionImage actual = auctionImageRepository.save(auctionImage); + // then em.flush(); em.clear(); + assertThat(actual.getId()).isPositive(); + } + + @Test + void 지정한_아이디에_해당하는_경매_이미지를_조회한다() { // when - final Optional actual = auctionImageRepository.findById(auctionImage.getId()); + final Optional actual = auctionImageRepository.findById(경매_이미지.getId()); // then - SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual).isPresent(); - softAssertions.assertThat(actual.get()).isEqualTo(auctionImage); - }); + assertThat(actual).contains(경매_이미지); } @Test void 지정한_아이디에_해당하는_경매_이미지가_없는_경우_빈_Optional을_반환한다() { - // given - final Long invalidAuctionImageId = -999L; - // when - final Optional actual = auctionImageRepository.findById(invalidAuctionImageId); + final Optional actual = auctionImageRepository.findById(존재하지_않는_경매_이미지_아이디); // then assertThat(actual).isEmpty(); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/JpaProfileImageRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/JpaProfileImageRepositoryTest.java new file mode 100644 index 000000000..59db38093 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/JpaProfileImageRepositoryTest.java @@ -0,0 +1,73 @@ +package com.ddang.ddang.image.infrastructure.persistence; + +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.image.infrastructure.persistence.fixture.JpaProfileImageRepositoryFixture; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class JpaProfileImageRepositoryTest extends JpaProfileImageRepositoryFixture { + + @PersistenceContext + EntityManager em; + + @Autowired + JpaProfileImageRepository profileImageRepository; + + @Test + void 프로필_이미지를_저장한다() { + // given + final ProfileImage profileImage = new ProfileImage(업로드_이미지_파일명, 저장된_이미지_파일명); + + // when + final ProfileImage actual = profileImageRepository.save(profileImage); + + em.flush(); + em.clear(); + + // then + assertThat(actual.getId()).isPositive(); + } + + @Test + void 지정한_아이디에_해당하는_이미지를_조회한다() { + // when + final Optional actual = profileImageRepository.findById(프로필_이미지.getId()); + + // then + assertThat(actual).contains(프로필_이미지); + } + + @Test + void 지정한_아이디에_해당하는_이미지가_없는_경우_빈_Optional을_반환한다() { + // when + final Optional actual = profileImageRepository.findById(존재하지_않는_프로필_이미지_아이디); + + // then + assertThat(actual).isEmpty(); + } + + @Test + void 저장된_이름에_해당하는_이미지를_반환한다() { + // when + final Optional actual = profileImageRepository.findByStoreName(저장된_이미지_파일명); + + // then + assertThat(actual).contains(프로필_이미지); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/JpaAuctionImageRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/JpaAuctionImageRepositoryFixture.java new file mode 100644 index 000000000..a92e8ec51 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/JpaAuctionImageRepositoryFixture.java @@ -0,0 +1,34 @@ +package com.ddang.ddang.image.infrastructure.persistence.fixture; + +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.infrastructure.persistence.JpaAuctionImageRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +@SuppressWarnings("NonAsciiCharacters") +public class JpaAuctionImageRepositoryFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaAuctionImageRepository auctionImageRepository; + + protected String 업로드_이미지_파일명 = "uploadName"; + protected String 저장된_이미지_파일명 = "storeName"; + protected Long 존재하지_않는_경매_이미지_아이디 = -999L; + + protected AuctionImage 경매_이미지; + + @BeforeEach + void setUp() { + 경매_이미지 = new AuctionImage(업로드_이미지_파일명, 저장된_이미지_파일명); + + auctionImageRepository.save(경매_이미지); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/JpaProfileImageRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/JpaProfileImageRepositoryFixture.java new file mode 100644 index 000000000..1bf175301 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/JpaProfileImageRepositoryFixture.java @@ -0,0 +1,34 @@ +package com.ddang.ddang.image.infrastructure.persistence.fixture; + +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.image.infrastructure.persistence.JpaProfileImageRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +@SuppressWarnings("NonAsciiCharacters") +public class JpaProfileImageRepositoryFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaProfileImageRepository profileImageRepository; + + protected String 업로드_이미지_파일명 = "uploadName"; + protected String 저장된_이미지_파일명 = "storeName"; + protected Long 존재하지_않는_프로필_이미지_아이디 = -999L; + + protected ProfileImage 프로필_이미지; + + @BeforeEach + void setUp() { + 프로필_이미지 = new ProfileImage("uploadName", "storeName"); + + profileImageRepository.save(프로필_이미지); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/ImageControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/ImageControllerTest.java index 74a2a4871..2b5a9978e 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/ImageControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/ImageControllerTest.java @@ -1,48 +1,26 @@ package com.ddang.ddang.image.presentation; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.BDDMockito.given; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.ddang.ddang.exception.GlobalExceptionHandler; -import com.ddang.ddang.image.application.ImageService; import com.ddang.ddang.image.application.exception.ImageNotFoundException; -import java.net.MalformedURLException; +import com.ddang.ddang.image.presentation.fixture.ImageControllerFixture; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -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.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; -import org.springframework.core.io.ByteArrayResource; -import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@WebMvcTest(controllers = {ImageController.class}, - excludeFilters = { - @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfigurer.class), - @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com\\.ddang\\.ddang\\.authentication\\.configuration\\..*") - } -) -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -@SuppressWarnings("NonAsciiCharacters") -class ImageControllerTest { - @MockBean - ImageService imageService; +import java.net.MalformedURLException; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @Autowired - ImageController imageController; +@SuppressWarnings("NonAsciiCharacters") +class ImageControllerTest extends ImageControllerFixture { MockMvc mockMvc; @@ -55,31 +33,58 @@ void setUp() { } @Test - void 지정한_아이디에_대한_경매_이미지를_조회한다() throws Exception { + void 지정한_사용자_아이디에_대한_사용자_이미지를_조회한다() throws Exception { + // given + given(imageService.readProfileImage(anyLong())).willReturn(이미지_파일_리소스); + + // when & then + mockMvc.perform(RestDocumentationRequestBuilders.get("/users/images/{id}", 프로필_이미지_아이디)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.IMAGE_JPEG)) + .andExpect(content().bytes(이미지_파일_바이트)); + } + + @Test + void 사용자_이미지_조회시_지정한_아이디에_대한_이미지가_없는_경우_404를_반환한다() throws Exception { + // given + given(imageService.readProfileImage(anyLong())).willThrow(new ImageNotFoundException("지정한 이미지를 찾을 수 없습니다.")); + + // when & then + mockMvc.perform(RestDocumentationRequestBuilders.get("/users/images/{id}", 존재하지_않는_프로필_이미지_아이디)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").exists()); + } + + @Test + void 사용자_이미지_조회시_유효한_프로토콜이나_URL이_아닌_경우_500을_반환한다() throws Exception { // given - final byte[] imageBytes = "이것은 이미지 파일의 바이트 코드입니다.".getBytes(); - final Resource mockResource = new ByteArrayResource(imageBytes); + given(imageService.readProfileImage(anyLong())).willThrow(new MalformedURLException()); - given(imageService.readAuctionImage(anyLong())).willReturn(mockResource); + // when & then + mockMvc.perform(RestDocumentationRequestBuilders.get("/users/images/{id}", 프로필_이미지_아이디)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.message").exists()); + } + + @Test + void 지정한_아이디에_대한_경매_이미지를_조회한다() throws Exception { + // given + given(imageService.readAuctionImage(anyLong())).willReturn(이미지_파일_리소스); // when & then - mockMvc.perform(RestDocumentationRequestBuilders.get("/auctions/images/{id}", 1L)) + mockMvc.perform(RestDocumentationRequestBuilders.get("/auctions/images/{id}", 경매_이미지_아이디)) .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.IMAGE_PNG)) - .andExpect(content().bytes(imageBytes)); + .andExpect(content().contentType(MediaType.IMAGE_JPEG)) + .andExpect(content().bytes(이미지_파일_바이트)); } @Test void 경매_이미지_조회시_지정한_아이디에_대한_이미지가_없는_경우_404를_반환한다() throws Exception { // given - final Long invalidAuctionImageId = 1L; - - given(imageService.readAuctionImage(anyLong())).willThrow(new ImageNotFoundException( - "지정한 이미지를 찾을 수 없습니다." - )); + given(imageService.readAuctionImage(anyLong())).willThrow(new ImageNotFoundException("지정한 이미지를 찾을 수 없습니다.")); // when & then - mockMvc.perform(RestDocumentationRequestBuilders.get("/auctions/images/{id}", invalidAuctionImageId)) + mockMvc.perform(RestDocumentationRequestBuilders.get("/auctions/images/{id}", 존재하지_않는_경매_이미지_아이디)) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.message").exists()); } @@ -87,12 +92,10 @@ void setUp() { @Test void 경매_이미지_조회시_유효한_프로토콜이나_URL이_아닌_경우_500을_반환한다() throws Exception { // given - final Long invalidAuctionImageId = 1L; - given(imageService.readAuctionImage(anyLong())).willThrow(new MalformedURLException()); // when & then - mockMvc.perform(RestDocumentationRequestBuilders.get("/auctions/images/{id}", invalidAuctionImageId)) + mockMvc.perform(RestDocumentationRequestBuilders.get("/auctions/images/{id}", 경매_이미지_아이디)) .andExpect(status().isInternalServerError()) .andExpect(jsonPath("$.message").exists()); } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/fixture/ImageControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/fixture/ImageControllerFixture.java new file mode 100644 index 000000000..dbac712ed --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/fixture/ImageControllerFixture.java @@ -0,0 +1,16 @@ +package com.ddang.ddang.image.presentation.fixture; + +import com.ddang.ddang.configuration.CommonControllerSliceTest; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; + +@SuppressWarnings("NonAsciiCharacters") +public class ImageControllerFixture extends CommonControllerSliceTest { + + protected byte[] 이미지_파일_바이트 = "이것은 이미지 파일의 바이트 코드입니다.".getBytes(); + protected Resource 이미지_파일_리소스 = new ByteArrayResource(이미지_파일_바이트); + protected Long 프로필_이미지_아이디 = 1L; + protected Long 존재하지_않는_프로필_이미지_아이디 = -999L; + protected Long 경매_이미지_아이디 = 1L; + protected Long 존재하지_않는_경매_이미지_아이디 = -999L; +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/util/ImageUrlCalculatorTest.java b/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/util/ImageUrlCalculatorTest.java new file mode 100644 index 000000000..741e064d2 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/util/ImageUrlCalculatorTest.java @@ -0,0 +1,31 @@ +package com.ddang.ddang.image.presentation.util; + +import com.ddang.ddang.image.presentation.util.fixture.ImageUrlCalculatorFixture; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class ImageUrlCalculatorTest extends ImageUrlCalculatorFixture { + + @Test + void 프로필_사진의_URL을_계산한다() { + // when + final String actual = ImageUrlCalculator.calculateBy(프로필_이미지_절대_URL, 프로필_이미지_아이디); + + // then + assertThat(actual).isEqualTo(프로필_이미지_전체_URL); + } + + @Test + void 경매_대표_이미지의_URL을_계산한다() { + // when + final String actual = ImageUrlCalculator.calculateBy(경매_이미지_절대_URL, 경매_이미지_아이디); + + // then + assertThat(actual).isEqualTo(경매_이미지_전체_URL); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/util/fixture/ImageUrlCalculatorFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/util/fixture/ImageUrlCalculatorFixture.java new file mode 100644 index 000000000..d817dd3a2 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/util/fixture/ImageUrlCalculatorFixture.java @@ -0,0 +1,12 @@ +package com.ddang.ddang.image.presentation.util.fixture; + +@SuppressWarnings("NonAsciiCharacters") +public class ImageUrlCalculatorFixture { + + protected String 프로필_이미지_절대_URL = "http://3-ddang.store/users/images/"; + protected Long 프로필_이미지_아이디 = 1L; + protected String 프로필_이미지_전체_URL = 프로필_이미지_절대_URL + 프로필_이미지_아이디; + protected String 경매_이미지_절대_URL = "http://3-ddang.store/auctions/images/"; + protected Long 경매_이미지_아이디 = 1L; + protected String 경매_이미지_전체_URL = 경매_이미지_절대_URL + 경매_이미지_아이디; +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/log/SlackAppenderTest.java b/backend/ddang/src/test/java/com/ddang/ddang/log/SlackAppenderTest.java index 78d083de6..638f7cdde 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/log/SlackAppenderTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/log/SlackAppenderTest.java @@ -1,6 +1,7 @@ package com.ddang.ddang.log; import ch.qos.logback.classic.spi.ILoggingEvent; +import com.ddang.ddang.configuration.log.SlackAppender; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; diff --git a/backend/ddang/src/test/java/com/ddang/ddang/notification/application/FcmNotificationServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/notification/application/FcmNotificationServiceTest.java new file mode 100644 index 000000000..e20fba2e3 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/notification/application/FcmNotificationServiceTest.java @@ -0,0 +1,75 @@ +package com.ddang.ddang.notification.application; + +import com.ddang.ddang.configuration.IsolateDatabase; +import com.ddang.ddang.configuration.fcm.exception.FcmNotFoundException; +import com.ddang.ddang.device.application.exception.DeviceTokenNotFoundException; +import com.ddang.ddang.notification.application.fixture.FcmNotificationServiceFixture; +import com.ddang.ddang.notification.domain.NotificationStatus; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.Message; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.ApplicationEventPublisher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +@IsolateDatabase +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class FcmNotificationServiceTest extends FcmNotificationServiceFixture { + + @MockBean + FirebaseMessaging firebaseMessaging; + + @Autowired + NotificationService notificationService; + + @Autowired + ApplicationEventPublisher eventPublisher; + + @Test + void 알림을_전송한다() throws FirebaseMessagingException { + // given + given(firebaseMessaging.send(any(Message.class))).willReturn(알림_메시지_아이디); + + final NotificationStatus actual = notificationService.send(알림_생성_DTO); + + // then + assertThat(actual).isEqualTo(NotificationStatus.SUCCESS); + } + + @Test + void 알림을_전송시_알림을_받을_사용자_기기_토큰을_찾을_수_없다면_예외를_던진다() throws FirebaseMessagingException { + // when & then + assertThatThrownBy(() -> notificationService.send(기기토큰이_없는_사용자의_알림_생성_DTO)) + .isInstanceOf(DeviceTokenNotFoundException.class) + .hasMessageContaining("사용자의 기기 토큰을 찾을 수 없습니다."); + } + + @Test + void Fcm에_의한_알림_전송_실패시_예외를_던진다() throws FirebaseMessagingException { + // given + given(firebaseMessaging.send(any(Message.class))).willThrow(FirebaseMessagingException.class); + + // when & then + assertThatThrownBy(() -> notificationService.send(알림_생성_DTO)) + .isInstanceOf(FirebaseMessagingException.class); + } + + @Test + void Fcm이_등록되지_않은_경우_알림_전송_실패시_예외를_던진다() throws FirebaseMessagingException { + // given + given(firebaseMessaging.send(any(Message.class))).willThrow(FcmNotFoundException.class); + + // when & then + assertThatThrownBy(() -> notificationService.send(알림_생성_DTO)) + .isInstanceOf(FcmNotFoundException.class); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/notification/application/NotificationEventListenerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/notification/application/NotificationEventListenerTest.java new file mode 100644 index 000000000..3cce5b25a --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/notification/application/NotificationEventListenerTest.java @@ -0,0 +1,131 @@ +package com.ddang.ddang.notification.application; + +import com.ddang.ddang.bid.application.BidService; +import com.ddang.ddang.bid.application.event.BidNotificationEvent; +import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository; +import com.ddang.ddang.chat.application.MessageService; +import com.ddang.ddang.chat.application.event.MessageNotificationEvent; +import com.ddang.ddang.chat.infrastructure.persistence.JpaMessageRepository; +import com.ddang.ddang.configuration.IsolateDatabase; +import com.ddang.ddang.notification.application.fixture.NotificationEventListenerFixture; +import com.ddang.ddang.notification.domain.NotificationStatus; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.event.ApplicationEvents; +import org.springframework.test.context.event.RecordApplicationEvents; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +@IsolateDatabase +@RecordApplicationEvents +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class NotificationEventListenerTest extends NotificationEventListenerFixture { + + @MockBean + FirebaseMessaging firebaseMessaging; + + @MockBean + NotificationService notificationService; + + @Autowired + NotificationEventListener notificationEventListener; + + @Autowired + ApplicationEvents events; + + @Autowired + MessageService messageService; + + @Autowired + JpaMessageRepository messageRepository; + + @Autowired + JpaBidRepository bidRepository; + + @Autowired + BidService bidService; + + @Test + void 이벤트가_호출되면_메시지_알림을_전송한다() throws FirebaseMessagingException { + // given + given(notificationService.send(any())).willReturn(NotificationStatus.SUCCESS); + + // when + notificationEventListener.sendMessageNotification(메시지_알림_이벤트); + + // then + verify(notificationService).send(any()); + } + + @Test + void 메시지를_전송하면_알림을_전송한다() { + // when + messageService.create(메시지_생성_DTO, 이미지_절대_경로); + + // then + final long actual = events.stream(MessageNotificationEvent.class).count(); + assertThat(actual).isEqualTo(1); + } + + @Test + void 메시지_알림_전송이_실패해도_메시지는_저장된다() throws FirebaseMessagingException { + // when + given(firebaseMessaging.send(any())).willThrow(FirebaseMessagingException.class); + final Long actualSavedMessageId = messageService.create(메시지_생성_DTO, 이미지_절대_경로); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(messageRepository.findById(actualSavedMessageId)).isPresent(); + + final long actual = events.stream(MessageNotificationEvent.class).count(); + softAssertions.assertThat(actual).isEqualTo(1); + }); + } + + @Test + void 상위_입찰자가_발생하면_이전_입찰자에게_알림을_전송한다() { + // when + bidService.create(입찰_생성_DTO, 이미지_절대_경로); + + // then + final long actual = events.stream(BidNotificationEvent.class).count(); + assertThat(actual).isEqualTo(1); + } + + @Test + void 입찰_알림_전송이_실패해도_메시지는_저장된다() throws FirebaseMessagingException { + // when + given(firebaseMessaging.send(any())).willThrow(FirebaseMessagingException.class); + final Long actualSavedMessageId = bidService.create(입찰_생성_DTO, 이미지_절대_경로); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(bidRepository.findById(actualSavedMessageId)).isPresent(); + + final long actual = events.stream(BidNotificationEvent.class).count(); + softAssertions.assertThat(actual).isEqualTo(1); + }); + } + + @Test + void 이벤트가_호출되면_입찰_알림을_전송한다() throws FirebaseMessagingException { + // given + given(notificationService.send(any())).willReturn(NotificationStatus.SUCCESS); + + // when + notificationEventListener.sendBidNotification(입찰_알림_이벤트); + + // then + verify(notificationService).send(any()); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/notification/application/fixture/FcmNotificationServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/notification/application/fixture/FcmNotificationServiceFixture.java new file mode 100644 index 000000000..2fc19c520 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/notification/application/fixture/FcmNotificationServiceFixture.java @@ -0,0 +1,147 @@ +package com.ddang.ddang.notification.application.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.bid.domain.Bid; +import com.ddang.ddang.bid.domain.BidPrice; +import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository; +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; +import com.ddang.ddang.chat.infrastructure.persistence.JpaMessageRepository; +import com.ddang.ddang.device.domain.DeviceToken; +import com.ddang.ddang.device.infrastructure.persistence.JpaDeviceTokenRepository; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.image.infrastructure.persistence.JpaAuctionImageRepository; +import com.ddang.ddang.notification.application.dto.CreateNotificationDto; +import com.ddang.ddang.notification.domain.NotificationType; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class FcmNotificationServiceFixture { + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaMessageRepository messageRepository; + + @Autowired + private JpaChatRoomRepository chatRoomRepository; + + @Autowired + private JpaDeviceTokenRepository deviceTokenRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaBidRepository bidRepository; + + @Autowired + private JpaAuctionImageRepository auctionImageRepository; + + protected User 메시지_조회자_겸_발신자; + private User 메시지_수신자; + private User 새로운_입찰자; + private User 기기토큰이_없는_사용자; + protected DeviceToken 기기토큰; + protected CreateNotificationDto 기기토큰이_없는_사용자의_알림_생성_DTO; + protected CreateNotificationDto 알림_생성_DTO; + + protected String 알림_메시지_아이디 = "notificationMessageId"; + + @BeforeEach + void setUp() { + 메시지_조회자_겸_발신자 = User.builder() + .name("메시지_조회자_겸_발신자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + 메시지_수신자 = User.builder() + .name("메시지_수신자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + 새로운_입찰자 = User.builder() + .name("입찰자1") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("56789") + .build(); + 기기토큰이_없는_사용자 = User.builder() + .name("기기토큰이 없는 사용자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12234") + .build(); + + userRepository.save(메시지_조회자_겸_발신자); + userRepository.save(메시지_수신자); + userRepository.save(새로운_입찰자); + userRepository.save(기기토큰이_없는_사용자); + + 기기토큰 = new DeviceToken(메시지_조회자_겸_발신자, "deviceToken"); + + deviceTokenRepository.save(기기토큰); + + 알림_생성_DTO = new CreateNotificationDto( + NotificationType.MESSAGE, + 메시지_조회자_겸_발신자.getId(), + "제목", + "내용", + "/redirectUrlForNotification", + "image.png" + ); + + 기기토큰이_없는_사용자의_알림_생성_DTO = new CreateNotificationDto( + NotificationType.MESSAGE, + 기기토큰이_없는_사용자.getId(), + "제목", + "내용", + "/redirectUrl", + "image.png" + ); + + final Auction 경매 = Auction.builder() + .seller(메시지_조회자_겸_발신자) + .title("경매글") + .description("경매글 설명") + .bidUnit(new BidUnit(100)) + .startPrice(new Price(100)) + .closingTime(LocalDateTime.now().plusDays(3L)) + .build(); + auctionRepository.save(경매); + + final AuctionImage 경매_이미지 = new AuctionImage("upload.jpg", "store.jpg"); + auctionImageRepository.save(경매_이미지); + 경매.addAuctionImages(List.of(경매_이미지)); + + final Bid bid = new Bid(경매, 새로운_입찰자, new BidPrice(200)); + bidRepository.save(bid); + 경매.updateLastBid(bid); + + final ChatRoom 채팅방 = new ChatRoom(경매, 메시지_조회자_겸_발신자); + chatRoomRepository.save(채팅방); + + final Message 메시지 = Message.builder() + .chatRoom(채팅방) + .writer(메시지_조회자_겸_발신자) + .receiver(메시지_수신자) + .contents("메시지") + .build(); + messageRepository.save(메시지); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/notification/application/fixture/NotificationEventListenerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/notification/application/fixture/NotificationEventListenerFixture.java new file mode 100644 index 000000000..72f007c9a --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/notification/application/fixture/NotificationEventListenerFixture.java @@ -0,0 +1,125 @@ +package com.ddang.ddang.notification.application.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.auction.infrastructure.persistence.dto.AuctionAndImageDto; +import com.ddang.ddang.bid.application.dto.BidDto; +import com.ddang.ddang.bid.application.dto.CreateBidDto; +import com.ddang.ddang.bid.application.event.BidNotificationEvent; +import com.ddang.ddang.bid.domain.Bid; +import com.ddang.ddang.bid.domain.BidPrice; +import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository; +import com.ddang.ddang.chat.application.dto.CreateMessageDto; +import com.ddang.ddang.chat.application.dto.MessageDto; +import com.ddang.ddang.chat.application.event.MessageNotificationEvent; +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; +import com.ddang.ddang.chat.infrastructure.persistence.JpaMessageRepository; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.image.infrastructure.persistence.JpaAuctionImageRepository; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class NotificationEventListenerFixture { + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaChatRoomRepository chatRoomRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaAuctionImageRepository auctionImageRepository; + + @Autowired + private JpaBidRepository bidRepository; + + @Autowired + private JpaMessageRepository messageRepository; + + protected CreateMessageDto 메시지_생성_DTO; + protected CreateBidDto 입찰_생성_DTO; + protected MessageNotificationEvent 메시지_알림_이벤트; + protected BidNotificationEvent 입찰_알림_이벤트; + + protected String 이미지_절대_경로 = "/imageUrl"; + + @BeforeEach + void setUp() { + final User 발신자_겸_판매자 = User.builder() + .name("발신자 겸 판매자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + final User 수신자_겸_기존_입찰자 = User.builder() + .name("수신자 겸 기존 입찰자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + final User 새로운_입찰자 = User.builder() + .name("새로운 입찰자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("13579") + .build(); + userRepository.save(발신자_겸_판매자); + userRepository.save(수신자_겸_기존_입찰자); + userRepository.save(새로운_입찰자); + + final Auction 경매 = Auction.builder() + .seller(발신자_겸_판매자) + .title("경매글") + .description("경매글 설명") + .bidUnit(new BidUnit(100)) + .startPrice(new Price(100)) + .closingTime(LocalDateTime.now().plusDays(3L)) + .build(); + auctionRepository.save(경매); + + final AuctionImage 경매_이미지 = new AuctionImage("upload.jpg", "store.jpg"); + auctionImageRepository.save(경매_이미지); + 경매.addAuctionImages(List.of(경매_이미지)); + + final ChatRoom 채팅방 = new ChatRoom(경매, 발신자_겸_판매자); + chatRoomRepository.save(채팅방); + + final Message 메시지 = Message.builder() + .chatRoom(채팅방) + .writer(발신자_겸_판매자) + .receiver(수신자_겸_기존_입찰자) + .contents("메시지 내용") + .build(); + final Message 저장된_메시지 = messageRepository.save(메시지); + + 메시지_생성_DTO = new CreateMessageDto(채팅방.getId(), 발신자_겸_판매자.getId(), 수신자_겸_기존_입찰자.getId(), "메시지 내용"); + + final Bid bid = new Bid(경매, 수신자_겸_기존_입찰자, new BidPrice(200)); + bidRepository.save(bid); + 경매.updateLastBid(bid); + + 입찰_생성_DTO = new CreateBidDto(경매.getId(), 1000, 새로운_입찰자.getId()); + + final MessageDto 메시지_DTO = MessageDto.of(저장된_메시지, 채팅방, 발신자_겸_판매자, 수신자_겸_기존_입찰자, 이미지_절대_경로); + 메시지_알림_이벤트 = new MessageNotificationEvent(메시지_DTO); + + final AuctionAndImageDto auctionAndImageDto = new AuctionAndImageDto(경매, 경매_이미지); + final BidDto 입찰_DTO = new BidDto(수신자_겸_기존_입찰자.getId(), auctionAndImageDto, 이미지_절대_경로); + 입찰_알림_이벤트 = new BidNotificationEvent(입찰_DTO); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/notification/application/util/NotificationPropertyTest.java b/backend/ddang/src/test/java/com/ddang/ddang/notification/application/util/NotificationPropertyTest.java new file mode 100644 index 000000000..6a295375f --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/notification/application/util/NotificationPropertyTest.java @@ -0,0 +1,25 @@ +package com.ddang.ddang.notification.application.util; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class NotificationPropertyTest { + + @Test + void 알림_메시지_key이름을_반환한다() { + // given + final NotificationProperty[] values = NotificationProperty.values(); + + // when & then + assertThat(values).containsAll(List.of( + NotificationProperty.NOTIFICATION_TYPE, + NotificationProperty.IMAGE, + NotificationProperty.TITLE, + NotificationProperty.BODY, + NotificationProperty.REDIRECT_URL + )); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/notification/domain/NotificationStatusTest.java b/backend/ddang/src/test/java/com/ddang/ddang/notification/domain/NotificationStatusTest.java new file mode 100644 index 000000000..79b1a1440 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/notification/domain/NotificationStatusTest.java @@ -0,0 +1,32 @@ +package com.ddang.ddang.notification.domain; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class NotificationStatusTest { + + @Test + void 정상적인_토큰이_전달된_경우_알림_상태_성공을_반환한다() { + // given + final String fcmReturnedValue = "successToken"; + + // when + final NotificationStatus expected = NotificationStatus.calculateStatus(fcmReturnedValue); + + //then + assertThat(expected).isEqualTo(NotificationStatus.SUCCESS); + } + + @Test + void null이_전달된_경우_알림_상태_실패를_반환한다() { + // given + final String fcmReturnedValue = null; + + // when + final NotificationStatus expected = NotificationStatus.calculateStatus(fcmReturnedValue); + + //then + assertThat(expected).isEqualTo(NotificationStatus.FAIL); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/qna/application/AnswerServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/qna/application/AnswerServiceTest.java new file mode 100644 index 000000000..f1f7a37d4 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/qna/application/AnswerServiceTest.java @@ -0,0 +1,100 @@ +package com.ddang.ddang.qna.application; + +import com.ddang.ddang.auction.application.exception.UserForbiddenException; +import com.ddang.ddang.configuration.IsolateDatabase; +import com.ddang.ddang.qna.application.exception.AlreadyAnsweredException; +import com.ddang.ddang.qna.application.exception.AnswerNotFoundException; +import com.ddang.ddang.qna.application.exception.InvalidAnswererException; +import com.ddang.ddang.qna.application.exception.QuestionNotFoundException; +import com.ddang.ddang.qna.application.fixture.AnswerServiceFixture; +import com.ddang.ddang.user.application.exception.UserNotFoundException; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@IsolateDatabase +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AnswerServiceTest extends AnswerServiceFixture { + + @Autowired + AnswerService answerService; + + @Test + void 답변을_등록한다() { + // when + final Long actual = answerService.create(답변_등록_요청_dto); + + // then + assertThat(actual).isPositive(); + } + + @Test + void 존재하지_않는_사용자가_질문에_답하는_경우_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> answerService.create(존재하지_않는_사용자의_답변_등록_요청_dto)) + .isInstanceOf(UserNotFoundException.class) + .hasMessage("해당 사용자를 찾을 수 없습니다."); + } + + @Test + void 존재하지_않는_질문에_답하는_경우_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> answerService.create(존재하지_않는_질문에_답변_등록_요청_dto)) + .isInstanceOf(QuestionNotFoundException.class) + .hasMessage("해당 질문을 찾을 수 없습니다."); + } + + @Test + void 판매자가_아닌_다른_사용자가_질문에_답하는_경우_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> answerService.create(판매자가_아닌_사용자가_질문에_답변_등록_요청_dto)) + .isInstanceOf(InvalidAnswererException.class) + .hasMessage("판매자만 답변할 수 있습니다."); + } + + @Test + void 이미_답변한_질문에_답하는_경우_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> answerService.create(이미_답변한_질문에_답변_등록_요청_dto)) + .isInstanceOf(AlreadyAnsweredException.class) + .hasMessage("이미 답변한 질문입니다."); + } + + @Test + void 답변을_삭제한다() { + // when + answerService.deleteById(답변.getId(), 판매자.getId()); + + // then + assertThat(답변.isDeleted()).isTrue(); + } + + @Test + void 존재하지_않는_답변_삭제시_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> answerService.deleteById(존재하지_않는_답변_아이디, 판매자.getId())) + .isInstanceOf(AnswerNotFoundException.class) + .hasMessage("해당 답변을 찾을 수 없습니다."); + } + + @Test + void 존재하지_않는_사용자가_답변_삭제시_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> answerService.deleteById(답변.getId(), 존재하지_않는_사용자_아이디)) + .isInstanceOf(UserNotFoundException.class) + .hasMessage("해당 사용자를 찾을 수 없습니다."); + } + + @Test + void 답변_작성자가_아닌_사용자가_답변_삭제시_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> answerService.deleteById(답변.getId(), 판매자가_아닌_사용자.getId())) + .isInstanceOf(UserForbiddenException.class) + .hasMessage("삭제할 권한이 없습니다."); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/qna/application/QuestionServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/qna/application/QuestionServiceTest.java new file mode 100644 index 000000000..c994b7f55 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/qna/application/QuestionServiceTest.java @@ -0,0 +1,145 @@ +package com.ddang.ddang.qna.application; + +import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; +import com.ddang.ddang.auction.application.exception.UserForbiddenException; +import com.ddang.ddang.configuration.IsolateDatabase; +import com.ddang.ddang.qna.application.dto.ReadQnaDto; +import com.ddang.ddang.qna.application.dto.ReadQnasDto; +import com.ddang.ddang.qna.application.exception.InvalidAuctionToAskQuestionException; +import com.ddang.ddang.qna.application.exception.InvalidQuestionerException; +import com.ddang.ddang.qna.application.exception.QuestionNotFoundException; +import com.ddang.ddang.qna.application.fixture.QuestionServiceFixture; +import com.ddang.ddang.user.application.exception.UserNotFoundException; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@IsolateDatabase +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class QuestionServiceTest extends QuestionServiceFixture { + + @Autowired + QuestionService questionService; + + @Test + void 질문을_등록한다() { + // when + final Long actual = questionService.create(경매_질문_등록_요청_dto); + + // then + assertThat(actual).isPositive(); + } + + @Test + void 존재하지_않는_사용자가_경매에_질문하는_경우_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> questionService.create(존재하지_않는_사용자가_경매_질문_등록_요청_dto)) + .isInstanceOf(UserNotFoundException.class) + .hasMessage("해당 사용자를 찾을 수 없습니다."); + } + + @Test + void 존재하지_않는_경매에_질문하는_경우_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> questionService.create(존재하지_않는_경매_질문_등록_요청_dto)) + .isInstanceOf(AuctionNotFoundException.class) + .hasMessage("해당 경매를 찾을 수 없습니다."); + } + + @Test + void 이미_종료된_경매에_질문하는_경우_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> questionService.create(종료된_경매_질문_등록_요청_dto)) + .isInstanceOf(InvalidAuctionToAskQuestionException.class) + .hasMessage("이미 종료된 경매입니다."); + } + + @Test + void 삭제된_경매에_질문하는_경우_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> questionService.create(삭제된_경매_질문_등록_요청_dto)) + .isInstanceOf(AuctionNotFoundException.class) + .hasMessage("해당 경매를 찾을 수 없습니다."); + } + + @Test + void 판매자가_본인_경매에_질문하는_경우_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> questionService.create(판매자가_본인_경매_질문_등록_요청_dto)) + .isInstanceOf(InvalidQuestionerException.class) + .hasMessage("경매 등록자는 질문할 수 없습니다."); + } + + @Test + void 경매_아이디를_통해_질문과_답변을_모두_조회한다() { + // when + final ReadQnasDto actual = questionService.readAllByAuctionId(질문_3개_답변_2개가_존재하는_경매_아이디); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + final List questionAndAnswerDtos = actual.readQnaDtos(); + softAssertions.assertThat(questionAndAnswerDtos).hasSize(3); + + final ReadQnaDto 첫번째_질문 = questionAndAnswerDtos.get(0); + softAssertions.assertThat(첫번째_질문.readQuestionDto()).isEqualTo(질문_정보_dto1); + softAssertions.assertThat(첫번째_질문.readAnswerDto()).isEqualTo(답변_정보_dto1); + + final ReadQnaDto 두번째_질문 = questionAndAnswerDtos.get(1); + softAssertions.assertThat(두번째_질문.readQuestionDto()).isEqualTo(질문_정보_dto2); + softAssertions.assertThat(두번째_질문.readAnswerDto()).isEqualTo(답변_정보_dto2); + + final ReadQnaDto 세번째_질문 = questionAndAnswerDtos.get(2); + softAssertions.assertThat(세번째_질문.readQuestionDto()).isEqualTo(질문_정보_dto3); + softAssertions.assertThat(세번째_질문.readAnswerDto()).isNull(); + }); + } + + @Test + void 존재하지_않는_경매_아이디를_통해_질문과_답변을_모두_조회할시_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> questionService.readAllByAuctionId(존재하지_않는_경매_아이디)) + .isInstanceOf(AuctionNotFoundException.class) + .hasMessage("해당 경매를 찾을 수 없습니다."); + } + + @Test + void 질문을_삭제한다() { + // when + questionService.deleteById(질문.getId(), 질문자.getId()); + + // then + assertThat(질문.isDeleted()).isTrue(); + } + + @Test + void 존재하지_않는_질문_삭제시_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> questionService.deleteById(존재하지_않는_질문_아이디, 질문자.getId())) + .isInstanceOf(QuestionNotFoundException.class) + .hasMessage("해당 질문을 찾을 수 없습니다."); + } + + @Test + void 존재하지_않는_사용자가_질문_삭제시_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> questionService.deleteById(질문.getId(), 존재하지_않는_사용자_아이디)) + .isInstanceOf(UserNotFoundException.class) + .hasMessage("해당 사용자를 찾을 수 없습니다."); + } + + @Test + void 질문_작성자가_아닌_사용자가_질문_삭제시_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> questionService.deleteById(질문.getId(), 질문하지_않은_사용자.getId())) + .isInstanceOf(UserForbiddenException.class) + .hasMessage("삭제할 권한이 없습니다."); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/qna/application/fixture/AnswerServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/qna/application/fixture/AnswerServiceFixture.java new file mode 100644 index 000000000..28afd100c --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/qna/application/fixture/AnswerServiceFixture.java @@ -0,0 +1,94 @@ +package com.ddang.ddang.qna.application.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.qna.application.dto.CreateAnswerDto; +import com.ddang.ddang.qna.domain.Answer; +import com.ddang.ddang.qna.domain.Question; +import com.ddang.ddang.qna.infrastructure.JpaAnswerRepository; +import com.ddang.ddang.qna.infrastructure.JpaQuestionRepository; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class AnswerServiceFixture { + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaQuestionRepository questionRepository; + + @Autowired + private JpaAnswerRepository answerRepository; + + protected Long 존재하지_않는_답변_아이디 = -999L; + protected Long 존재하지_않는_사용자_아이디 = -999L; + protected Answer 답변; + protected User 판매자; + protected User 판매자가_아닌_사용자; + + protected CreateAnswerDto 답변_등록_요청_dto; + protected CreateAnswerDto 존재하지_않는_사용자의_답변_등록_요청_dto; + protected CreateAnswerDto 존재하지_않는_질문에_답변_등록_요청_dto; + protected CreateAnswerDto 판매자가_아닌_사용자가_질문에_답변_등록_요청_dto; + protected CreateAnswerDto 이미_답변한_질문에_답변_등록_요청_dto; + + @BeforeEach + void setUp() { + final ProfileImage 프로필_이미지 = new ProfileImage("프로필.jpg", "프로필.jpg"); + 판매자 = User.builder() + .name("판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + final Auction 경매 = Auction.builder() + .seller(판매자) + .title("경매 상품 1") + .description("이것은 경매 상품 1 입니다.") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now().plusDays(7)) + .build(); + final User 질문자 = User.builder() + .name("질문자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + 판매자가_아닌_사용자 = User.builder() + .name("판매자가 아닌 사용자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + final Question 질문 = new Question(경매, 질문자, "궁금한 점이 있습니다."); + final Question 답변한_질문 = new Question(경매, 질문자, "궁금한 점이 있습니다."); + 답변 = new Answer("답변드립니다."); + 답변한_질문.addAnswer(답변); + + userRepository.saveAll(List.of(판매자, 질문자, 판매자가_아닌_사용자)); + auctionRepository.save(경매); + questionRepository.saveAll(List.of(질문, 답변한_질문)); + answerRepository.save(답변); + + 답변_등록_요청_dto = new CreateAnswerDto(질문.getId(), "답변 드립니다.", 판매자.getId()); + 존재하지_않는_사용자의_답변_등록_요청_dto = new CreateAnswerDto(질문.getId(), "답변 드립니다.", -999L); + 존재하지_않는_질문에_답변_등록_요청_dto = new CreateAnswerDto(-999L, "답변 드립니다.", 판매자.getId()); + 판매자가_아닌_사용자가_질문에_답변_등록_요청_dto = new CreateAnswerDto(질문.getId(), "답변 드립니다.", 질문자.getId()); + 이미_답변한_질문에_답변_등록_요청_dto = new CreateAnswerDto(답변한_질문.getId(), "답변 드립니다.", 판매자.getId()); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/qna/application/fixture/QuestionServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/qna/application/fixture/QuestionServiceFixture.java new file mode 100644 index 000000000..215c975fa --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/qna/application/fixture/QuestionServiceFixture.java @@ -0,0 +1,143 @@ +package com.ddang.ddang.qna.application.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.qna.application.dto.CreateQuestionDto; +import com.ddang.ddang.qna.application.dto.ReadAnswerDto; +import com.ddang.ddang.qna.application.dto.ReadQuestionDto; +import com.ddang.ddang.qna.application.dto.ReadUserInQnaDto; +import com.ddang.ddang.qna.domain.Answer; +import com.ddang.ddang.qna.domain.Question; +import com.ddang.ddang.qna.infrastructure.JpaAnswerRepository; +import com.ddang.ddang.qna.infrastructure.JpaQuestionRepository; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class QuestionServiceFixture { + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaQuestionRepository questionRepository; + + @Autowired + private JpaAnswerRepository answerRepository; + + protected Long 질문_3개_답변_2개가_존재하는_경매_아이디; + protected Long 존재하지_않는_경매_아이디 = -999L; + protected Long 존재하지_않는_질문_아이디 = -999L; + protected Long 존재하지_않는_사용자_아이디 = -999L; + protected Question 질문; + protected User 질문자; + protected User 질문하지_않은_사용자; + protected CreateQuestionDto 경매_질문_등록_요청_dto; + protected CreateQuestionDto 존재하지_않는_사용자가_경매_질문_등록_요청_dto; + protected CreateQuestionDto 존재하지_않는_경매_질문_등록_요청_dto; + protected CreateQuestionDto 종료된_경매_질문_등록_요청_dto; + protected CreateQuestionDto 삭제된_경매_질문_등록_요청_dto; + protected CreateQuestionDto 판매자가_본인_경매_질문_등록_요청_dto; + protected ReadQuestionDto 질문_정보_dto1; + protected ReadQuestionDto 질문_정보_dto2; + protected ReadQuestionDto 질문_정보_dto3; + protected ReadAnswerDto 답변_정보_dto1; + protected ReadAnswerDto 답변_정보_dto2; + + @BeforeEach + void setUp() { + final ProfileImage 프로필_이미지 = new ProfileImage("프로필.jpg", "프로필.jpg"); + final User 판매자 = User.builder() + .name("판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + final Auction 경매 = Auction.builder() + .seller(판매자) + .title("경매 상품 1") + .description("이것은 경매 상품 1 입니다.") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now().plusDays(7)) + .build(); + final Auction 질문과_답변이_존재하는_경매 = Auction.builder() + .seller(판매자) + .title("경매 상품 1") + .description("이것은 경매 상품 1 입니다.") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now().plusDays(7)) + .build(); + final Auction 종료된_경매 = Auction.builder() + .seller(판매자) + .title("경매 상품 1") + .description("이것은 경매 상품 1 입니다.") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now().minusDays(7)) + .build(); + final Auction 삭제된_경매 = Auction.builder() + .seller(판매자) + .title("경매 상품 1") + .description("이것은 경매 상품 1 입니다.") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now().plusDays(7)) + .build(); + 삭제된_경매.delete(); + 질문자 = User.builder() + .name("질문자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + 질문하지_않은_사용자 = User.builder() + .name("사용자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + 질문 = new Question(질문과_답변이_존재하는_경매, 질문자, "질문1"); + final Question 질문2 = new Question(질문과_답변이_존재하는_경매, 질문자, "질문2"); + final Question 질문3 = new Question(질문과_답변이_존재하는_경매, 질문자, "질문3"); + final Answer 답변1 = new Answer("답변1"); + final Answer 답변2 = new Answer("답변2"); + 질문.addAnswer(답변1); + 질문2.addAnswer(답변2); + + userRepository.saveAll(List.of(판매자, 질문자, 질문하지_않은_사용자)); + auctionRepository.saveAll(List.of(경매, 질문과_답변이_존재하는_경매, 종료된_경매, 삭제된_경매)); + questionRepository.saveAll(List.of(질문, 질문2, 질문3)); + answerRepository.saveAll(List.of(답변1, 답변2)); + + 질문_3개_답변_2개가_존재하는_경매_아이디 = 질문과_답변이_존재하는_경매.getId(); + + 경매_질문_등록_요청_dto = new CreateQuestionDto(경매.getId(), "궁금한 점이 있습니다.", 질문자.getId()); + 존재하지_않는_사용자가_경매_질문_등록_요청_dto = new CreateQuestionDto(경매.getId(), "궁금한 점이 있습니다.", -999L); + 존재하지_않는_경매_질문_등록_요청_dto = new CreateQuestionDto(-999L, "궁금한 점이 있습니다.", 질문자.getId()); + 종료된_경매_질문_등록_요청_dto = new CreateQuestionDto(종료된_경매.getId(), "궁금한 점이 있습니다.", 질문자.getId()); + 삭제된_경매_질문_등록_요청_dto = new CreateQuestionDto(삭제된_경매.getId(), "궁금한 점이 있습니다.", 질문자.getId()); + 판매자가_본인_경매_질문_등록_요청_dto = new CreateQuestionDto(경매.getId(), "궁금한 점이 있습니다.", 판매자.getId()); + + final ReadUserInQnaDto 판매자_정보_dto = ReadUserInQnaDto.from(판매자); + final ReadUserInQnaDto 질문자_정보_dto = ReadUserInQnaDto.from(질문자); + 질문_정보_dto1 = new ReadQuestionDto(질문.getId(), 질문자_정보_dto, 질문.getContent(), 질문.getCreatedTime()); + 질문_정보_dto2 = new ReadQuestionDto(질문2.getId(), 질문자_정보_dto, 질문2.getContent(), 질문2.getCreatedTime()); + 질문_정보_dto3 = new ReadQuestionDto(질문3.getId(), 질문자_정보_dto, 질문3.getContent(), 질문3.getCreatedTime()); + 답변_정보_dto1 = new ReadAnswerDto(답변1.getId(), 판매자_정보_dto, 답변1.getContent(), 답변1.getCreatedTime()); + 답변_정보_dto2 = new ReadAnswerDto(답변2.getId(), 판매자_정보_dto, 답변2.getContent(), 답변2.getCreatedTime()); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/qna/domain/AnswerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/qna/domain/AnswerTest.java new file mode 100644 index 000000000..a6d4228e4 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/qna/domain/AnswerTest.java @@ -0,0 +1,71 @@ +package com.ddang.ddang.qna.domain; + +import com.ddang.ddang.qna.domain.fixture.AnswerFixture; +import com.ddang.ddang.user.domain.User; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AnswerTest extends AnswerFixture { + + @Test + void 답변과_질문의_연관관계를_세팅한다() { + // given + final Answer answer = new Answer("답변드립니다."); + + // when + answer.initQuestion(질문); + + // then + assertThat(answer.getQuestion()).isEqualTo(질문); + } + + @Test + void 답변의_작성자_즉_판매자라면_참을_반환한다() { + // when + final boolean actual = 답변.isWriter(판매자); + + // then + assertThat(actual).isTrue(); + } + + @Test + void 답변의_작성자가_아니라면_거짓을_반환한다() { + // when + final boolean actual = 답변.isWriter(답변_작성자가_아닌_사용자); + + // then + assertThat(actual).isFalse(); + } + + @Test + void 삭제되지_않은_답변이라면_삭제_여부_확인시_거짓을_반환한다() { + // when + final boolean actual = 답변.isDeleted(); + + // then + assertThat(actual).isFalse(); + } + + @Test + void 삭제된_답변이라면_삭제_여부_확인시_참을_반환한다() { + // when + final boolean actual = 삭제된_답변.isDeleted(); + + // then + assertThat(actual).isTrue(); + } + + @Test + void 답변의_작성자_조회시_질문에_해당하는_경매의_판매자_정보를_가져온다() { + // when + final User actual = 답변.getWriter(); + + // then + assertThat(actual).isEqualTo(판매자); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/qna/domain/QuestionTest.java b/backend/ddang/src/test/java/com/ddang/ddang/qna/domain/QuestionTest.java new file mode 100644 index 000000000..21d3175f0 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/qna/domain/QuestionTest.java @@ -0,0 +1,89 @@ +package com.ddang.ddang.qna.domain; + +import com.ddang.ddang.qna.domain.fixture.QuestionFixture; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class QuestionTest extends QuestionFixture { + + @Test + void 질문과_답변의_연관관계를_세팅한다() { + // when + 질문.addAnswer(답변); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(질문.getAnswer()).isEqualTo(답변); + softAssertions.assertThat(답변.getQuestion()).isEqualTo(질문); + }); + } + + @Test + void 답변_가능한_사용자인지_확인할때_판매자인_경우_참을_반환한다() { + // when + final boolean actual = 질문.isAnsweringAllowed(판매자); + + // then + assertThat(actual).isTrue(); + } + + @Test + void 답변_가능한_사용자인지_확인할때_판매자가_아닌_경우_거짓을_반환한다() { + // when + final boolean actual = 질문.isAnsweringAllowed(판매자가_아닌_사용자); + + // then + assertThat(actual).isFalse(); + } + + @Test + void 질문_작성자라면_참을_반환한다() { + // when + final boolean actual = 질문.isWriter(질문_작성자); + + // given + assertThat(actual).isTrue(); + } + + @Test + void 질문_작성자라면_거짓을_반환한다() { + // when + final boolean actual = 질문.isWriter(작성자가_아닌_사용자); + + // given + assertThat(actual).isFalse(); + } + + @Test + void 질문을_삭제한다() { + // when + 질문.delete(); + + // given + assertThat(질문.isDeleted()).isTrue(); + } + + @Test + void 삭제되지_않은_질문의_삭제_여부_확인시_거짓을_반환한다() { + // when + final boolean actual = 질문.isDeleted(); + + // then + assertThat(actual).isFalse(); + } + + @Test + void 삭제된_질문의_삭제_여부_확인시_참을_반환한다() { + // when + final boolean actual = 삭제된_질문.isDeleted(); + + // then + assertThat(actual).isTrue(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/qna/domain/fixture/AnswerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/qna/domain/fixture/AnswerFixture.java new file mode 100644 index 000000000..0099a5a5f --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/qna/domain/fixture/AnswerFixture.java @@ -0,0 +1,54 @@ +package com.ddang.ddang.qna.domain.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.qna.domain.Answer; +import com.ddang.ddang.qna.domain.Question; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDateTime; + +@SuppressWarnings("NonAsciiCharacters") +public class AnswerFixture { + + protected User 판매자 = User.builder() + .name("판매자") + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + private Auction 경매 = Auction.builder() + .seller(판매자) + .title("경매 상품") + .description("이것은 경매 상품입니다.") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + private User 질문_작성자 = User.builder() + .name("질문 작성자") + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + protected User 답변_작성자가_아닌_사용자 = 질문_작성자; + + protected Question 질문 = new Question(경매, 질문_작성자, "궁금한 점이 있어요."); + private Question 답변이_있는_질문 = new Question(경매, 질문_작성자, "궁금한 점이 있어요."); + protected Answer 답변 = new Answer("답변드립니다."); + protected Answer 삭제된_답변 = new Answer("답변드립니다."); + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(판매자, "id", 1L); + ReflectionTestUtils.setField(질문, "id", 1L); + ReflectionTestUtils.setField(답변이_있는_질문, "id", 1L); + ReflectionTestUtils.setField(답변, "id", 1L); + ReflectionTestUtils.setField(삭제된_답변, "id", 1L); + + 답변이_있는_질문.addAnswer(답변); + 삭제된_답변.delete(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/qna/domain/fixture/QuestionFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/qna/domain/fixture/QuestionFixture.java new file mode 100644 index 000000000..deee8b3eb --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/qna/domain/fixture/QuestionFixture.java @@ -0,0 +1,56 @@ +package com.ddang.ddang.qna.domain.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.qna.domain.Answer; +import com.ddang.ddang.qna.domain.Question; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDateTime; + +@SuppressWarnings("NonAsciiCharacters") +public class QuestionFixture { + + private ProfileImage 프로필_이미지 = new ProfileImage("프로필.jpg", "프로필.jpg"); + protected User 판매자 = User.builder() + .name("판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + private Auction 경매 = Auction.builder() + .seller(판매자) + .title("경매 상품") + .description("이것은 경매 상품입니다.") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + protected User 질문_작성자 = User.builder() + .name("질문 작성자") + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + protected User 작성자가_아닌_사용자 = 판매자; + private String 질문_내용 = "궁금한 점이 있어요."; + protected User 판매자가_아닌_사용자 = 질문_작성자; + + protected Question 질문 = new Question(경매, 질문_작성자, 질문_내용); + protected Question 삭제된_질문 = new Question(경매, 질문_작성자, 질문_내용); + protected Answer 답변 = new Answer("답변드립니다."); + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(판매자, "id", 1L); + ReflectionTestUtils.setField(질문_작성자, "id", 2L); + ReflectionTestUtils.setField(질문, "id", 1L); + ReflectionTestUtils.setField(답변, "id", 1L); + + 삭제된_질문.delete(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/qna/infrastructure/JpaAnswerRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/qna/infrastructure/JpaAnswerRepositoryTest.java new file mode 100644 index 000000000..bd3a53533 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/qna/infrastructure/JpaAnswerRepositoryTest.java @@ -0,0 +1,75 @@ +package com.ddang.ddang.qna.infrastructure; + +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.ddang.ddang.qna.domain.Answer; +import com.ddang.ddang.qna.infrastructure.fixture.JpaAnswerRepositoryFixture; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class JpaAnswerRepositoryTest extends JpaAnswerRepositoryFixture { + + @Autowired + JpaAnswerRepository answerRepository; + + @Test + void 답변을_저장한다() { + // given + final Answer answer = new Answer(답변_내용); + 질문.addAnswer(answer); + + // when + final Answer actual = answerRepository.save(answer); + + // then + assertThat(actual.getId()).isPositive(); + } + + @Test + void 이미_질문에_대한_답변이_존재한다면_참을_반환한다() { + // when + final boolean actual = answerRepository.existsByQuestionId(답변이_존재하는_질문.getId()); + + // then + assertThat(actual).isTrue(); + } + + @Test + void 이미_질문에_대한_답변이_존재하지_않는다면_거짓을_반환한다() { + // when + final boolean actual = answerRepository.existsByQuestionId(답변이_존재하지_않는_질문.getId()); + + // then + assertThat(actual).isFalse(); + } + + @Test + void 삭제된_답변은_조회되지_않는다() { + // when + final Optional actual = answerRepository.findByIdAndDeletedIsFalse(삭제된_답변.getId()); + + // then + assertThat(actual).isEmpty(); + } + + @Test + void 삭제되지_않은_답변은_조회된다() { + // when + final Optional actual = answerRepository.findByIdAndDeletedIsFalse(답변.getId()); + + // then + assertThat(actual).contains(답변); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/qna/infrastructure/JpaQuestionRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/qna/infrastructure/JpaQuestionRepositoryTest.java new file mode 100644 index 000000000..f3ff3c557 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/qna/infrastructure/JpaQuestionRepositoryTest.java @@ -0,0 +1,75 @@ +package com.ddang.ddang.qna.infrastructure; + +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.ddang.ddang.qna.domain.Question; +import com.ddang.ddang.qna.infrastructure.fixture.JpaQuestionRepositoryFixture; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class JpaQuestionRepositoryTest extends JpaQuestionRepositoryFixture { + + @Autowired + JpaQuestionRepository questionRepository; + + @Test + void 질문을_저장한다() { + // given + final Question question = new Question(경매, 질문자, 질문_내용); + + // when + final Question actual = questionRepository.save(question); + + // then + assertThat(actual.getId()).isPositive(); + } + + @Test + void 삭제된_질문은_조회되지_않는다() { + // when + final Optional actual = questionRepository.findByIdAndDeletedIsFalse(삭제된_질문.getId()); + + // then + assertThat(actual).isEmpty(); + } + + @Test + void 삭제되지_않은_질문은_조회된다() { + // when + final Optional actual = questionRepository.findByIdAndDeletedIsFalse(질문1.getId()); + + // then + assertThat(actual).contains(질문1); + } + + @Test + void 경매_아이디를_통해_질문과_답변들을_모두_조회한다() { + // when + final List actual = questionRepository.findAllByAuctionId(질문이_3개_답변이_2개인_경매.getId()); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.get(0)).isEqualTo(질문1); + softAssertions.assertThat(actual.get(0).getAnswer()).isEqualTo(답변1); + softAssertions.assertThat(actual.get(1)).isEqualTo(질문2); + softAssertions.assertThat(actual.get(1).getAnswer()).isEqualTo(답변2); + softAssertions.assertThat(actual.get(2)).isEqualTo(질문3); + softAssertions.assertThat(actual.get(2).getAnswer()).isNull(); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/qna/infrastructure/fixture/JpaAnswerRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/qna/infrastructure/fixture/JpaAnswerRepositoryFixture.java new file mode 100644 index 000000000..524ef1cae --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/qna/infrastructure/fixture/JpaAnswerRepositoryFixture.java @@ -0,0 +1,90 @@ +package com.ddang.ddang.qna.infrastructure.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.qna.domain.Answer; +import com.ddang.ddang.qna.domain.Question; +import com.ddang.ddang.qna.infrastructure.JpaAnswerRepository; +import com.ddang.ddang.qna.infrastructure.JpaQuestionRepository; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class JpaAnswerRepositoryFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaQuestionRepository questionRepository; + + @Autowired + private JpaAnswerRepository answerRepository; + + protected Question 질문; + protected Question 답변이_존재하는_질문; + protected Question 답변이_존재하지_않는_질문; + protected String 답변_내용 = "답변드립니다."; + protected Answer 답변; + protected Answer 삭제된_답변; + + @BeforeEach + void setUp() { + final ProfileImage 프로필_이미지 = new ProfileImage("프로필.jpg", "프로필.jpg"); + final User 판매자 = User.builder() + .name("판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + final Auction 경매 = Auction.builder() + .seller(판매자) + .title("경매 상품 1") + .description("이것은 경매 상품 1 입니다.") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + final User 질문자 = User.builder() + .name("질문자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + 질문 = new Question(경매, 질문자, "궁금한 점이 있어요."); + 답변이_존재하는_질문 = new Question(경매, 질문자, "궁금한 점이 있어요."); + 답변이_존재하지_않는_질문 = 질문; + final Question 답변이_삭제된_질문 = new Question(경매, 질문자, "궁금한 점이 있어요."); + + 답변 = new Answer("답변드립니다."); + 답변이_존재하는_질문.addAnswer(답변); + 삭제된_답변 = new Answer("답변드립니다."); + 답변이_삭제된_질문.addAnswer(삭제된_답변); + 삭제된_답변.delete(); + + userRepository.saveAll(List.of(판매자, 질문자)); + auctionRepository.save(경매); + questionRepository.saveAll(List.of(질문, 답변이_존재하는_질문, 답변이_삭제된_질문)); + answerRepository.saveAll(List.of(답변, 삭제된_답변)); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/qna/infrastructure/fixture/JpaQuestionRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/qna/infrastructure/fixture/JpaQuestionRepositoryFixture.java new file mode 100644 index 000000000..40a7ceeda --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/qna/infrastructure/fixture/JpaQuestionRepositoryFixture.java @@ -0,0 +1,103 @@ +package com.ddang.ddang.qna.infrastructure.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.qna.domain.Answer; +import com.ddang.ddang.qna.domain.Question; +import com.ddang.ddang.qna.infrastructure.JpaAnswerRepository; +import com.ddang.ddang.qna.infrastructure.JpaQuestionRepository; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class JpaQuestionRepositoryFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaQuestionRepository questionRepository; + + @Autowired + private JpaAnswerRepository answerRepository; + + protected Auction 경매; + protected Auction 질문이_3개_답변이_2개인_경매; + protected User 질문자; + protected String 질문_내용 = "궁금한 점이 있어요."; + protected Question 질문1; + protected Question 질문2; + protected Question 질문3; + protected Question 삭제된_질문; + protected Answer 답변1; + protected Answer 답변2; + + @BeforeEach + void setUp() { + final ProfileImage 프로필_이미지 = new ProfileImage("프로필.jpg", "프로필.jpg"); + final User 판매자 = User.builder() + .name("판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + 경매 = Auction.builder() + .seller(판매자) + .title("경매 상품") + .description("이것은 경매 상품입니다.") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 질문이_3개_답변이_2개인_경매 = Auction.builder() + .seller(판매자) + .title("경매 상품") + .description("이것은 경매 상품입니다.") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 질문자 = User.builder() + .name("질문자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + + 질문1 = new Question(질문이_3개_답변이_2개인_경매, 질문자, "질문1"); + 질문2 = new Question(질문이_3개_답변이_2개인_경매, 질문자, "질문2"); + 질문3 = new Question(질문이_3개_답변이_2개인_경매, 질문자, "질문3"); + 답변1 = new Answer("답변1"); + 답변2 = new Answer("답변2"); + 질문1.addAnswer(답변1); + 질문2.addAnswer(답변2); + + 삭제된_질문 = new Question(경매, 질문자, "질문3"); + 삭제된_질문.delete(); + + userRepository.saveAll(List.of(판매자, 질문자)); + auctionRepository.saveAll(List.of(경매, 질문이_3개_답변이_2개인_경매)); + questionRepository.saveAll(List.of(질문1, 질문2, 질문3, 삭제된_질문)); + answerRepository.saveAll(List.of(답변1, 답변2)); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/qna/presentation/QnaControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/qna/presentation/QnaControllerTest.java new file mode 100644 index 000000000..df9614eff --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/qna/presentation/QnaControllerTest.java @@ -0,0 +1,609 @@ +package com.ddang.ddang.qna.presentation; + +import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; +import com.ddang.ddang.auction.application.exception.UserForbiddenException; +import com.ddang.ddang.auction.configuration.DescendingSortPageableArgumentResolver; +import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; +import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; +import com.ddang.ddang.authentication.domain.TokenDecoder; +import com.ddang.ddang.authentication.domain.TokenType; +import com.ddang.ddang.authentication.domain.dto.AuthenticationStore; +import com.ddang.ddang.exception.GlobalExceptionHandler; +import com.ddang.ddang.qna.application.dto.CreateAnswerDto; +import com.ddang.ddang.qna.application.dto.CreateQuestionDto; +import com.ddang.ddang.qna.application.exception.AlreadyAnsweredException; +import com.ddang.ddang.qna.application.exception.AnswerNotFoundException; +import com.ddang.ddang.qna.application.exception.InvalidAnswererException; +import com.ddang.ddang.qna.application.exception.InvalidAuctionToAskQuestionException; +import com.ddang.ddang.qna.application.exception.InvalidQuestionerException; +import com.ddang.ddang.qna.application.exception.QuestionNotFoundException; +import com.ddang.ddang.qna.presentation.dto.request.CreateAnswerRequest; +import com.ddang.ddang.qna.presentation.dto.request.CreateQuestionRequest; +import com.ddang.ddang.qna.presentation.fixture.QnaControllerFixture; +import com.ddang.ddang.user.application.exception.UserNotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.Optional; +import java.util.stream.Stream; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SuppressWarnings("NonAsciiCharacters") +class QnaControllerTest extends QnaControllerFixture { + + TokenDecoder tokenDecoder; + + MockMvc mockMvc; + + @BeforeEach + void setUp() { + tokenDecoder = mock(TokenDecoder.class); + + final AuthenticationStore store = new AuthenticationStore(); + final AuthenticationInterceptor interceptor = new AuthenticationInterceptor( + blackListTokenService, + authenticationUserService, + tokenDecoder, + store + ); + final AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(store); + + mockMvc = MockMvcBuilders.standaloneSetup(qnaController) + .setControllerAdvice(new GlobalExceptionHandler()) + .addInterceptors(interceptor) + .setCustomArgumentResolvers(resolver, new DescendingSortPageableArgumentResolver()) + .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) + .alwaysDo(print()) + .alwaysDo(restDocs) + .build(); + } + + @Test + void 질문을_등록한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(questionService.create(any(CreateQuestionDto.class))).willReturn(생성된_질문_아이디); + + // when & then + final ResultActions resultActions = mockMvc.perform(post("/questions") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(질문_등록_request)) + ) + .andExpectAll( + status().isCreated(), + header().string(HttpHeaders.LOCATION, is("/auctions/1")) + ); + + createQuestion_문서화(resultActions); + } + + @Test + void 경매_아이디를_입력하지_않은_경우_질문시_400을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + + // when & then + mockMvc.perform(post("/questions") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(경매_아이디가_없는_질문_등록_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @Test + void 경매_아이디를_양수가_아닌_경우_질문시_400을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + + // when & then + mockMvc.perform(post("/questions") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(경매_아이디가_음수인_질문_등록_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @ParameterizedTest + @MethodSource("provideQuestionRequestWithEmptyContent") + void 질문을_입력하지_않은_경우_질문시_400을_반환한다(final CreateQuestionRequest conte질문_내용이_빈값인_질문_등록_request) throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + + // when & then + mockMvc.perform(post("/questions") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(conte질문_내용이_빈값인_질문_등록_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + private static Stream provideQuestionRequestWithEmptyContent() { + return Stream.of(질문_내용이_null인_질문_등록_request, 질문_내용이_빈값인_질문_등록_request); + } + + @Test + void 존재하지_않은_사용자가_질문시_404를_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(존재하지_않는_사용자_ID_클레임)); + given(questionService.create(any(CreateQuestionDto.class))) + .willThrow(new UserNotFoundException("해당 사용자를 찾을 수 없습니다.")); + + // when & then + mockMvc.perform(post("/questions") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(질문_등록_request)) + ) + .andExpectAll( + status().isNotFound(), + jsonPath("$.message").exists() + ); + } + + @Test + void 존재하지_않은_경매에_질문시_404를_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(questionService.create(any(CreateQuestionDto.class))) + .willThrow(new AuctionNotFoundException("해당 경매를 찾을 수 없습니다.")); + + // when & then + mockMvc.perform(post("/questions") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(존재하지_않는_경매에_대한_질문_등록_request)) + ) + .andExpectAll( + status().isNotFound(), + jsonPath("$.message").exists() + ); + } + + @Test + void 이미_종료된_경매에_질문시_400을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(questionService.create(any(CreateQuestionDto.class))) + .willThrow(new InvalidAuctionToAskQuestionException("이미 종료된 경매입니다")); + + // when & then + mockMvc.perform(post("/questions") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(종료된_경매에_대한_질문_등록_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @Test + void 삭제된_경매에_질문시_400을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(questionService.create(any(CreateQuestionDto.class))) + .willThrow(new InvalidAuctionToAskQuestionException("삭제된 경매입니다")); + + // when & then + mockMvc.perform(post("/questions") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(삭제된_경매에_대한_질문_등록_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @Test + void 경매자가_본인이_등록한_경매에_질문시_400을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(questionService.create(any(CreateQuestionDto.class))) + .willThrow(new InvalidQuestionerException("경매 등록자는 질문할 수 없습니다")); + + // when & then + mockMvc.perform(post("/questions") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(판매자가_본인_경매에_대한_질문_등록_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @Test + void 답변을_등록한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(answerService.create(any(CreateAnswerDto.class))).willReturn(생성된_답변_아이디); + + // when & then + final ResultActions resultActions = + mockMvc.perform(RestDocumentationRequestBuilders.post("/questions/{questionId}/answers", 질문_아이디) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(답변_등록_request)) + ) + .andExpectAll( + status().isCreated(), + header().string(HttpHeaders.LOCATION, is("/auctions/1")) + ); + + createAnswer_문서화(resultActions); + } + + @Test + void 경매_아이디를_입력하지_않은_경우_답변시_400을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + + // when & then + mockMvc.perform(post("/questions/{questionId}/answers", 질문_아이디) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(경매_아이디가_없는_답변_등록_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @Test + void 경매_아이디가_양수가_아닌_경우_답변시_400을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + + // when & then + mockMvc.perform(post("/questions/{questionId}/answers", 질문_아이디) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(양수가_아닌_경매_아이디에_대한_답변_등록_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @ParameterizedTest + @MethodSource("provideAnswerRequestWithEmptyContent") + void 답변을_입력하지_않은_경우_답변시_400을_반환한다(final CreateAnswerRequest 답변_내용이_빈값인_답변_등록_request) throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + + // when & then + mockMvc.perform(post("/questions/{questionId}/answers", 질문_아이디) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(답변_내용이_빈값인_답변_등록_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + private static Stream provideAnswerRequestWithEmptyContent() { + return Stream.of(답변_내용이_null인_답변_등록_request, 답변_내용이_빈값인_답변_등록_request); + } + + @Test + void 존재하지_않은_사용자가_답변시_404을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(존재하지_않는_사용자_ID_클레임)); + given(answerService.create(any(CreateAnswerDto.class))) + .willThrow(new UserNotFoundException("해당 사용자를 찾을 수 없습니다.")); + + // when & then + mockMvc.perform(post("/questions/{questionId}/answers", 질문_아이디) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(답변_등록_request)) + ) + .andExpectAll( + status().isNotFound(), + jsonPath("$.message").exists() + ); + } + + @Test + void 존재하지_않은_질문에_답변시_404을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(answerService.create(any(CreateAnswerDto.class))) + .willThrow(new QuestionNotFoundException("해당 질문을 찾을 수 없습니다.")); + + // when & then + mockMvc.perform(post("/questions/{questionId}/answers", 존재하지_않는_질문_아이디) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(답변_등록_request)) + ) + .andExpectAll( + status().isNotFound(), + jsonPath("$.message").exists() + ); + } + + @Test + void 판매자가_아닌_사용자가_질문에_답변시_400을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(answerService.create(any(CreateAnswerDto.class))) + .willThrow(new InvalidAnswererException("판매자만 답변할 수 있습니다.")); + + // when & then + mockMvc.perform(post("/questions/{questionId}/answers", 질문_아이디) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(판매자가_아닌_사용자가_질문에_대한_답변_등록_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @Test + void 이미_답변한_질문에_답변시_400을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(answerService.create(any(CreateAnswerDto.class))) + .willThrow(new AlreadyAnsweredException("이미 답변한 질문입니다.")); + + // when & then + mockMvc.perform(post("/questions/{questionId}/answers", 질문_아이디) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(이미_답변한_질문에_대한_답변_등록_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @Test + void 질문을_삭제한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + willDoNothing().given(questionService).deleteById(anyLong(), anyLong()); + + // when & then + final ResultActions resultActions = + mockMvc.perform(RestDocumentationRequestBuilders.delete("/questions/{questionId}", 질문_아이디) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + ) + .andExpectAll( + status().isNoContent() + ); + + deleteQuestion_문서화(resultActions); + } + + @Test + void 존재하지_않는_질문을_삭제시_404를_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + willThrow(new QuestionNotFoundException("해당 질문을 찾을 수 없습니다.")) + .given(questionService).deleteById(anyLong(), anyLong()); + + // when & then + mockMvc.perform(delete("/questions/{questionId}", 존재하지_않는_질문_아이디) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + ) + .andExpectAll( + status().isNotFound(), + jsonPath("$.message").exists() + ); + } + + @Test + void 존재하지_않는_사용자가_질문_삭제시_404를_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(존재하지_않는_사용자_ID_클레임)); + willThrow(new UserNotFoundException("해당 사용자를 찾을 수 없습니다.")) + .given(questionService).deleteById(anyLong(), anyLong()); + + // when & then + mockMvc.perform(delete("/questions/{questionId}", 질문_아이디) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + ) + .andExpectAll( + status().isNotFound(), + jsonPath("$.message").exists() + ); + } + + @Test + void 작성자가_아닌_사용자가_질문_삭제시_401을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + willThrow(new UserForbiddenException("삭제할 권한이 없습니다.")) + .given(questionService).deleteById(anyLong(), anyLong()); + + // when & then + mockMvc.perform(delete("/questions/{questionId}", 질문_아이디) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + ) + .andExpectAll( + status().isUnauthorized(), + jsonPath("$.message").exists() + ); + } + + @Test + void 답변을_삭제한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + willDoNothing().given(answerService).deleteById(anyLong(), anyLong()); + + // when & then + final ResultActions resultActions = + mockMvc.perform(RestDocumentationRequestBuilders.delete("/questions/answers/{answerId}", 답변_아이디) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + ) + .andExpectAll( + status().isNoContent() + ); + + deleteAnswer_문서화(resultActions); + } + + @Test + void 존재하지_않는_답변을_삭제시_404를_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + willThrow(new AnswerNotFoundException("해당 답변을 찾을 수 없습니다.")) + .given(answerService).deleteById(anyLong(), anyLong()); + + // when & then + mockMvc.perform(delete("/questions/answers/{answerId}", 존재하지_않는_답변_아이디) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + ) + .andExpectAll( + status().isNotFound(), + jsonPath("$.message").exists() + ); + } + + @Test + void 존재하지_않는_사용자가_답변_삭제시_404를_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(존재하지_않는_사용자_ID_클레임)); + willThrow(new UserNotFoundException("해당 사용자를 찾을 수 없습니다.")) + .given(answerService).deleteById(anyLong(), anyLong()); + + // when & then + mockMvc.perform(delete("/questions/answers/{answerId}", 답변_아이디) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + ) + .andExpectAll( + status().isNotFound(), + jsonPath("$.message").exists() + ); + } + + @Test + void 작성자가_아닌_사용자가_답변_삭제시_401을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + willThrow(new UserForbiddenException("삭제할 권한이 없습니다.")) + .given(answerService).deleteById(anyLong(), anyLong()); + + // when & then + mockMvc.perform(delete("/questions/answers/{answerId}", 답변_아이디) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + ) + .andExpectAll( + status().isUnauthorized(), + jsonPath("$.message").exists() + ); + } + + public void createQuestion_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + requestFields( + fieldWithPath("auctionId").description("질문할 경매 ID"), + fieldWithPath("content").description("질문 내용") + ) + ) + ); + } + + public void createAnswer_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + pathParameters( + parameterWithName("questionId").description("답변할 질문 ID") + ), + requestFields( + fieldWithPath("auctionId").description("답변할 질문의 경매 ID"), + fieldWithPath("content").description("답변 내용") + ) + ) + ); + } + + public void deleteQuestion_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + pathParameters( + parameterWithName("questionId").description("삭제할 질문 ID") + ) + ) + ); + } + + private void deleteAnswer_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + pathParameters( + parameterWithName("answerId").description("삭제할 답변 ID") + ) + ) + ); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/qna/presentation/fixture/QnaControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/qna/presentation/fixture/QnaControllerFixture.java new file mode 100644 index 000000000..51e7dd519 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/qna/presentation/fixture/QnaControllerFixture.java @@ -0,0 +1,38 @@ +package com.ddang.ddang.qna.presentation.fixture; + +import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims; +import com.ddang.ddang.configuration.CommonControllerSliceTest; +import com.ddang.ddang.qna.presentation.dto.request.CreateAnswerRequest; +import com.ddang.ddang.qna.presentation.dto.request.CreateQuestionRequest; + +@SuppressWarnings("NonAsciiCharacters") +public class QnaControllerFixture extends CommonControllerSliceTest { + + protected PrivateClaims 사용자_ID_클레임 = new PrivateClaims(1L); + protected PrivateClaims 존재하지_않는_사용자_ID_클레임 = new PrivateClaims(999L); + protected Long 생성된_질문_아이디 = 1L; + protected Long 생성된_답변_아이디 = 1L; + protected Long 질문_아이디 = 1L; + protected Long 답변_아이디 = 1L; + protected Long 존재하지_않는_질문_아이디 = 999L; + protected Long 존재하지_않는_답변_아이디 = 999L; + protected String 액세스_토큰_값 = "Bearer accessToken"; + + protected CreateQuestionRequest 질문_등록_request = new CreateQuestionRequest(1L, "궁금한 점이 있습니다."); + protected CreateQuestionRequest 경매_아이디가_없는_질문_등록_request = new CreateQuestionRequest(null, "궁금한 점이 있습니다."); + protected CreateQuestionRequest 경매_아이디가_음수인_질문_등록_request = new CreateQuestionRequest(-1L, "궁금한 점이 있습니다."); + protected static CreateQuestionRequest 질문_내용이_null인_질문_등록_request = new CreateQuestionRequest(1L, null); + protected static CreateQuestionRequest 질문_내용이_빈값인_질문_등록_request = new CreateQuestionRequest(1L, ""); + protected CreateQuestionRequest 존재하지_않는_경매에_대한_질문_등록_request = new CreateQuestionRequest(999L, "궁금한 점이 있습니다."); + protected CreateQuestionRequest 종료된_경매에_대한_질문_등록_request = 질문_등록_request; + protected CreateQuestionRequest 삭제된_경매에_대한_질문_등록_request = 질문_등록_request; + protected CreateQuestionRequest 판매자가_본인_경매에_대한_질문_등록_request = 질문_등록_request; + + protected CreateAnswerRequest 답변_등록_request = new CreateAnswerRequest(1L, "답변 드립니다."); + protected CreateAnswerRequest 경매_아이디가_없는_답변_등록_request = new CreateAnswerRequest(null, "답변 드립니다."); + protected CreateAnswerRequest 양수가_아닌_경매_아이디에_대한_답변_등록_request = new CreateAnswerRequest(-1L, "답변 드립니다."); + protected static CreateAnswerRequest 답변_내용이_null인_답변_등록_request = new CreateAnswerRequest(-1L, null); + protected static CreateAnswerRequest 답변_내용이_빈값인_답변_등록_request = new CreateAnswerRequest(-1L, "null"); + protected CreateAnswerRequest 판매자가_아닌_사용자가_질문에_대한_답변_등록_request = new CreateAnswerRequest(1L, "답변 드립니다."); + protected CreateAnswerRequest 이미_답변한_질문에_대한_답변_등록_request = new CreateAnswerRequest(1L, "답변 드립니다."); +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/region/application/RegionServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/region/application/RegionServiceTest.java index a120897bf..017f1bca1 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/region/application/RegionServiceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/region/application/RegionServiceTest.java @@ -1,15 +1,11 @@ package com.ddang.ddang.region.application; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.BDDMockito.given; - import com.ddang.ddang.region.application.dto.ReadRegionDto; import com.ddang.ddang.region.application.exception.RegionNotFoundException; +import com.ddang.ddang.region.application.fixture.RegionServiceFixture; import com.ddang.ddang.region.domain.InitializationRegionProcessor; import com.ddang.ddang.region.domain.Region; import com.ddang.ddang.region.infrastructure.persistence.JpaRegionRepository; -import java.util.List; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; @@ -19,11 +15,16 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Transactional @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class RegionServiceTest { +class RegionServiceTest extends RegionServiceFixture { @MockBean InitializationRegionProcessor regionProcessor; @@ -37,13 +38,7 @@ class RegionServiceTest { @Test void 대한민국_전국의_지역을_초기화한다() { // given - final Region firstRegion = new Region("서울특별시"); - final Region secondRegion = new Region("송파구"); - final Region thirdRegion = new Region("가락1동"); - - secondRegion.addThirdRegion(thirdRegion); - firstRegion.addSecondRegion(secondRegion); - given(regionProcessor.requestRegions()).willReturn(List.of(firstRegion)); + given(regionProcessor.requestRegions()).willReturn(List.of(서울특별시, 두번째_지역이_없는_첫번째_지역)); // when regionService.createRegions(); @@ -51,113 +46,78 @@ class RegionServiceTest { // then final List actual = regionRepository.findAll(); - SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual).hasSize(3); - - softAssertions.assertThat(actual.get(0)).isEqualTo(firstRegion); - softAssertions.assertThat(actual.get(0).getSecondRegions()).hasSize(1); - - final Region actualSecondRegion = actual.get(0).getSecondRegions().get(0); - softAssertions.assertThat(actualSecondRegion).isEqualTo(secondRegion); - softAssertions.assertThat(actualSecondRegion.getThirdRegions()).hasSize(1); + final Region actualFirstRegion1 = actual.get(0); + final Region actualFirstRegion2 = actual.get(5); + final Region actualSecondRegion1OfFirstRegion1 = actualFirstRegion1.getSecondRegions().get(0); + final Region actualSecondRegion2OfFirstRegion1 = actualFirstRegion1.getSecondRegions().get(1); + final Region actualThirdRegion1OfSecondRegion1 = actualSecondRegion1OfFirstRegion1.getThirdRegions().get(0); + final Region actualThirdRegion2OfSecondRegion1 = actualSecondRegion1OfFirstRegion1.getThirdRegions().get(1); - final Region actualThirdRegion = actualSecondRegion.getThirdRegions().get(0); - softAssertions.assertThat(actualThirdRegion).isEqualTo(thirdRegion); + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(6); + softAssertions.assertThat(actualFirstRegion1).isEqualTo(서울특별시); + softAssertions.assertThat(actualFirstRegion2).isEqualTo(두번째_지역이_없는_첫번째_지역); + softAssertions.assertThat(actualFirstRegion1.getSecondRegions()).hasSize(2); + softAssertions.assertThat(actualSecondRegion1OfFirstRegion1).isEqualTo(서울특별시_강남구); + softAssertions.assertThat(actualSecondRegion2OfFirstRegion1).isEqualTo(세번째_지역이_없는_두번째_지역); + softAssertions.assertThat(actualSecondRegion1OfFirstRegion1.getThirdRegions()).hasSize(2); + softAssertions.assertThat(actualThirdRegion1OfSecondRegion1).isEqualTo(서울특별시_강남구_삼성동); + softAssertions.assertThat(actualThirdRegion2OfSecondRegion1).isEqualTo(서울특별시_강남구_대치동); }); } @Test void 모든_첫번째_지역을_조회한다() { - // given - final Region first1 = new Region("first1"); - final Region first2 = new Region("first2"); - final Region second = new Region("second"); - - first1.addSecondRegion(second); - - regionRepository.save(first1); - regionRepository.save(first2); - // when final List actual = regionService.readAllFirst(); // then - assertThat(actual).hasSize(2); - } - - @Test - void 첫번째_지역이_없는_경우_지역_조회시_예외가_발생한다() { - // when & then - assertThatThrownBy(() -> regionService.readAllFirst()) - .isInstanceOf(RegionNotFoundException.class) - .hasMessage("등록된 지역이 없습니다."); + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(2); + softAssertions.assertThat(actual.get(0).id()).isEqualTo(서울특별시.getId()); + softAssertions.assertThat(actual.get(1).id()).isEqualTo(두번째_지역이_없는_첫번째_지역.getId()); + }); } @Test void 첫번째_지역에_해당하는_모든_두번째_지역을_조회한다() { - // given - final Region first = new Region("first"); - final Region second1 = new Region("second1"); - final Region second2 = new Region("second2"); - - first.addSecondRegion(second1); - first.addSecondRegion(second2); - - regionRepository.save(first); - // when - final List actual = regionService.readAllSecondByFirstRegionId(first.getId()); + final List actual = regionService.readAllSecondByFirstRegionId(서울특별시.getId()); // then - assertThat(actual).hasSize(2); + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(2); + softAssertions.assertThat(actual.get(0).id()).isEqualTo(서울특별시_강남구.getId()); + softAssertions.assertThat(actual.get(1).id()).isEqualTo(세번째_지역이_없는_두번째_지역.getId()); + }); } @Test void 지정한_첫번째_지역에_해당하는_두번째_지역이_없는_경우_두번째_지역_조회시_예외가_발생한다() { - // given - final Region first = new Region("first"); - - regionRepository.save(first); - // when & then - assertThatThrownBy(() -> regionService.readAllSecondByFirstRegionId(first.getId())) + assertThatThrownBy(() -> regionService.readAllSecondByFirstRegionId(두번째_지역이_없는_첫번째_지역.getId())) .isInstanceOf(RegionNotFoundException.class) .hasMessage("지정한 첫 번째 지역에 해당하는 두 번째 지역이 없습니다."); } @Test void 두번째_지역에_해당하는_모든_세번째_지역을_조회한다() { - // given - final Region first = new Region("first"); - final Region second = new Region("second"); - final Region third1 = new Region("third1"); - final Region third2 = new Region("third2"); - - first.addSecondRegion(second); - second.addThirdRegion(third1); - second.addThirdRegion(third2); - - regionRepository.save(first); - // when - final List actual = regionService.readAllThirdByFirstAndSecondRegionId(first.getId(), second.getId()); + final List actual = + regionService.readAllThirdByFirstAndSecondRegionId(서울특별시.getId(), 서울특별시_강남구.getId()); // then - assertThat(actual).hasSize(2); + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(2); + softAssertions.assertThat(actual.get(0).id()).isEqualTo(서울특별시_강남구_삼성동.getId()); + softAssertions.assertThat(actual.get(1).id()).isEqualTo(서울특별시_강남구_대치동.getId()); + }); } @Test void 지정한_첫번째와_두번째_지역에_해당하는_세번째_지역이_없는_경우_세번째_지역_조회시_예외가_발생한다() { - // given - final Region first = new Region("first"); - final Region second = new Region("second"); - - first.addSecondRegion(second); - - regionRepository.save(first); - // when & then - assertThatThrownBy(() -> regionService.readAllThirdByFirstAndSecondRegionId(first.getId(), second.getId())) + assertThatThrownBy(() -> regionService.readAllThirdByFirstAndSecondRegionId(서울특별시.getId(), 세번째_지역이_없는_두번째_지역.getId())) .isInstanceOf(RegionNotFoundException.class) .hasMessage("지정한 첫 번째와 두 번째 지역에 해당하는 세 번째 지역이 없습니다."); } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/region/application/fixture/RegionServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/region/application/fixture/RegionServiceFixture.java new file mode 100644 index 000000000..4a55a76e9 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/region/application/fixture/RegionServiceFixture.java @@ -0,0 +1,39 @@ +package com.ddang.ddang.region.application.fixture; + +import com.ddang.ddang.region.domain.Region; +import com.ddang.ddang.region.infrastructure.persistence.JpaRegionRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +@SuppressWarnings("NonAsciiCharacters") +public class RegionServiceFixture { + + @Autowired + private JpaRegionRepository regionRepository; + + protected Region 서울특별시; + protected Region 두번째_지역이_없는_첫번째_지역; + protected Region 서울특별시_강남구; + protected Region 세번째_지역이_없는_두번째_지역; + protected Region 서울특별시_강남구_삼성동; + protected Region 서울특별시_강남구_대치동; + + @BeforeEach + void setUp() { + 서울특별시 = new Region("서울특별시"); + 두번째_지역이_없는_첫번째_지역 = new Region("두번째 지역이 없는 첫번째 지역"); + 서울특별시_강남구 = new Region("강남구"); + 세번째_지역이_없는_두번째_지역 = new Region("세번째 지역이 없는 두번째 지역"); + 서울특별시_강남구_삼성동 = new Region("삼성동"); + 서울특별시_강남구_대치동 = new Region("대치동"); + + 서울특별시.addSecondRegion(서울특별시_강남구); + 서울특별시.addSecondRegion(세번째_지역이_없는_두번째_지역); + + 서울특별시_강남구.addThirdRegion(서울특별시_강남구_삼성동); + 서울특별시_강남구.addThirdRegion(서울특별시_강남구_대치동); + + regionRepository.save(서울특별시); + regionRepository.save(두번째_지역이_없는_첫번째_지역); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/region/domain/AuctionRegionTest.java b/backend/ddang/src/test/java/com/ddang/ddang/region/domain/AuctionRegionTest.java index 9cabe6be1..e9edbfdb5 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/region/domain/AuctionRegionTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/region/domain/AuctionRegionTest.java @@ -1,36 +1,25 @@ package com.ddang.ddang.region.domain; -import static org.assertj.core.api.Assertions.assertThat; - -import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.region.domain.fixture.AuctionRegionFixture; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class AuctionRegionTest { +class AuctionRegionTest extends AuctionRegionFixture { @Test void 직거래_지역과_경매의_연관관계를_세팅한다() { // given - final Region firstRegion = new Region("서울특별시"); - final Region secondRegion = new Region("강남구"); - final Region thirdRegion = new Region("역삼동"); - - secondRegion.addThirdRegion(thirdRegion); - firstRegion.addSecondRegion(secondRegion); - - final AuctionRegion auctionRegion = new AuctionRegion(firstRegion); - - final Auction auction = Auction.builder() - .title("title") - .build(); + final AuctionRegion auctionRegion = new AuctionRegion(서울특별시_하위_강남구_하위_역삼동); // when - auctionRegion.initAuction(auction); + auctionRegion.initAuction(경매); // then - assertThat(auctionRegion.getAuction()).isNotNull(); + assertThat(auctionRegion.getAuction().getTitle()).isEqualTo(경매.getTitle()); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/region/domain/RegionTest.java b/backend/ddang/src/test/java/com/ddang/ddang/region/domain/RegionTest.java index ace3aba71..86198e3f0 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/region/domain/RegionTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/region/domain/RegionTest.java @@ -20,10 +20,9 @@ class RegionTest { // then SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(first.getSecondRegions()) - .hasSize(1); - softAssertions.assertThat(second.getFirstRegion()) - .isNotNull(); + softAssertions.assertThat(first.getSecondRegions()).hasSize(1); + softAssertions.assertThat(first.getSecondRegions().get(0).getName()).isEqualTo(second.getName()); + softAssertions.assertThat(second.getFirstRegion().getName()).isEqualTo(first.getName()); }); } @@ -41,12 +40,10 @@ class RegionTest { // then SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(second.getThirdRegions()) - .hasSize(1); - softAssertions.assertThat(third.getSecondRegion()) - .isNotNull(); - softAssertions.assertThat(third.getFirstRegion()) - .isNotNull(); + softAssertions.assertThat(second.getThirdRegions()).hasSize(1); + softAssertions.assertThat(second.getThirdRegions().get(0).getName()).isEqualTo(third.getName()); + softAssertions.assertThat(third.getSecondRegion().getName()).isEqualTo(second.getName()); + softAssertions.assertThat(third.getFirstRegion().getName()).isEqualTo(first.getName()); }); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/region/domain/fixture/AuctionRegionFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/region/domain/fixture/AuctionRegionFixture.java new file mode 100644 index 000000000..bd31d95ab --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/region/domain/fixture/AuctionRegionFixture.java @@ -0,0 +1,26 @@ +package com.ddang.ddang.region.domain.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.region.domain.Region; +import org.junit.jupiter.api.BeforeEach; + +@SuppressWarnings("NonAsciiCharacters") +public class AuctionRegionFixture { + + protected Region 서울특별시_하위_강남구_하위_역삼동; + protected Auction 경매; + + @BeforeEach + void setUp() { + Region 서울특별시 = new Region("서울특별시"); + Region 서울특별시_하위_강남구 = new Region("강남구"); + 서울특별시_하위_강남구_하위_역삼동 = new Region("역삼동"); + + 서울특별시_하위_강남구.addThirdRegion(서울특별시_하위_강남구_하위_역삼동); + 서울특별시.addSecondRegion(서울특별시_하위_강남구); + + 경매 = Auction.builder() + .title("title") + .build(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/region/infrastructure/api/RestTemplateInitRegionProcessorTest.java b/backend/ddang/src/test/java/com/ddang/ddang/region/infrastructure/api/RestTemplateInitRegionProcessorTest.java index cc4f9eefa..6cc6fb5e4 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/region/infrastructure/api/RestTemplateInitRegionProcessorTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/region/infrastructure/api/RestTemplateInitRegionProcessorTest.java @@ -1,20 +1,10 @@ package com.ddang.ddang.region.infrastructure.api; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.hamcrest.Matchers.matchesPattern; -import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; -import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; - import com.ddang.ddang.configuration.RestTemplateConfiguration; import com.ddang.ddang.region.domain.Region; -import com.ddang.ddang.region.infrastructure.api.dto.ApiAccessTokenResponse; -import com.ddang.ddang.region.infrastructure.api.dto.ResultApiRegionResponse; -import com.ddang.ddang.region.infrastructure.api.dto.TotalApiRegionResponse; import com.ddang.ddang.region.infrastructure.api.exception.RegionApiException; +import com.ddang.ddang.region.infrastructure.api.fixture.RestTemplateInitRegionProcessorFixture; import com.fasterxml.jackson.databind.ObjectMapper; -import java.util.Collections; -import java.util.List; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; @@ -27,11 +17,19 @@ import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.RestTemplate; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.hamcrest.Matchers.matchesPattern; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + @RestClientTest({RestTemplateInitRegionProcessor.class}) @Import(RestTemplateConfiguration.class) @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class RestTemplateInitRegionProcessorTest { +class RestTemplateInitRegionProcessorTest extends RestTemplateInitRegionProcessorFixture { MockRestServiceServer mockRestServiceServer; @@ -52,77 +50,33 @@ void setUp() { @Test void API_요청을_통해_대한민국_전국_지역을_조회한다() throws Exception { // given - final String accessToken = "accessToken"; - final ApiAccessTokenResponse accessTokenResponse = new ApiAccessTokenResponse( - Collections.singletonMap("accessToken", accessToken) - ); - mockRestServiceServer .expect(requestTo(matchesPattern( - "https://sgisapi.kostat.go.kr/OpenAPI3/auth/authentication.json.*" + 액세스_토큰_요청_URI_패턴 ))) .andRespond(withSuccess( - objectMapper.writeValueAsString(accessTokenResponse), + objectMapper.writeValueAsString(유효한_액세스_토큰), MediaType.APPLICATION_JSON )); - final String firstRegionUri = "https://sgisapi.kostat.go.kr/OpenAPI3/addr/stage.json?" + - "accessToken=" + - accessToken; - final ResultApiRegionResponse firstResultApiRegionResponse = new ResultApiRegionResponse( - "서울특별시", - "11" - ); - final TotalApiRegionResponse openApiResultApiRegionResponse = new TotalApiRegionResponse( - List.of(firstResultApiRegionResponse) - ); - mockRestServiceServer - .expect(requestTo(firstRegionUri)) + .expect(requestTo(첫번쪠_지역_목록_조회_URI)) .andRespond(withSuccess( - objectMapper.writeValueAsString(openApiResultApiRegionResponse), + objectMapper.writeValueAsString(첫번째_지역_목록_조회_응답), MediaType.APPLICATION_JSON )); - final String secondRegionCode = "11"; - final String secondRegionUri = "https://sgisapi.kostat.go.kr/OpenAPI3/addr/stage.json?" + - "accessToken=" + - accessToken + - "&cd=" + - secondRegionCode; - final ResultApiRegionResponse secondResultApiRegionResponse = new ResultApiRegionResponse( - "강남구", - "11230" - ); - final TotalApiRegionResponse secondOpenApiResultApiRegionResponse = new TotalApiRegionResponse( - List.of(secondResultApiRegionResponse) - ); - mockRestServiceServer - .expect(requestTo(secondRegionUri)) + .expect(requestTo(서울특별시_하위_지역_목록_조회_URI)) .andRespond(withSuccess( - objectMapper.writeValueAsString(secondOpenApiResultApiRegionResponse), + objectMapper.writeValueAsString(서울특별시_하위_지역_목록_조회_응답), MediaType.APPLICATION_JSON) ); - final String thirdRegionCode = "11230"; - final String thirdRegionUri = "https://sgisapi.kostat.go.kr/OpenAPI3/addr/stage.json?" + - "accessToken=" + - accessToken + - "&cd=" + - thirdRegionCode; - final ResultApiRegionResponse thirdResultApiRegionResponse = new ResultApiRegionResponse( - "개포1동", - "11230680" - ); - final TotalApiRegionResponse thirdOpenApiResultApiRegionResponse = new TotalApiRegionResponse( - List.of(thirdResultApiRegionResponse) - ); - mockRestServiceServer - .expect(requestTo(thirdRegionUri)) + .expect(requestTo(서울특별시_강남구_하위_지역_목록_조회_URI)) .andRespond(withSuccess( - objectMapper.writeValueAsString(thirdOpenApiResultApiRegionResponse), + objectMapper.writeValueAsString(서울특별시_강남구의_하위_지역_목록_조회_응답), MediaType.APPLICATION_JSON )); @@ -134,29 +88,27 @@ void setUp() { softAssertions.assertThat(actual).hasSize(1); final Region actualFirstRegion = actual.get(0); - softAssertions.assertThat(actualFirstRegion.getName()).isEqualTo(firstResultApiRegionResponse.getRegionName()); + softAssertions.assertThat(actualFirstRegion.getName()).isEqualTo(서울특별시.getRegionName()); softAssertions.assertThat(actualFirstRegion.getSecondRegions()).hasSize(1); final Region actualSecondRegion = actualFirstRegion.getSecondRegions().get(0); - softAssertions.assertThat(actualSecondRegion.getName()).isEqualTo(secondResultApiRegionResponse.getRegionName()); + softAssertions.assertThat(actualSecondRegion.getName()).isEqualTo(강남구.getRegionName()); softAssertions.assertThat(actualSecondRegion.getThirdRegions()).hasSize(1); final Region actualThirdRegion = actualSecondRegion.getThirdRegions().get(0); - assertThat(actualThirdRegion.getName()).isEqualTo(thirdResultApiRegionResponse.getRegionName()); + assertThat(actualThirdRegion.getName()).isEqualTo(개포1동.getRegionName()); }); } @Test void service키나_secret키가_유효하지_않은_경우_예외가_발생한다() throws Exception { // given - final ApiAccessTokenResponse invalidAccessTokenResponse = new ApiAccessTokenResponse(null); - mockRestServiceServer .expect(requestTo(matchesPattern( - "https://sgisapi.kostat.go.kr/OpenAPI3/auth/authentication.json.*" + 액세스_토큰_요청_URI_패턴 ))) .andRespond(withSuccess( - objectMapper.writeValueAsString(invalidAccessTokenResponse), + objectMapper.writeValueAsString(유효하지_않은_액세스_토큰), MediaType.APPLICATION_JSON )); @@ -169,23 +121,18 @@ void setUp() { @Test void 단계별_주소_조회_API_요청에_실패하면_예외가_발생한다() throws Exception { // given - final String accessToken = "accessToken"; - final ApiAccessTokenResponse accessTokenResponse = new ApiAccessTokenResponse( - Collections.singletonMap("accessToken", accessToken) - ); - mockRestServiceServer .expect(requestTo(matchesPattern( - "https://sgisapi.kostat.go.kr/OpenAPI3/auth/authentication.json.*" + 액세스_토큰_요청_URI_패턴 ))) .andRespond(withSuccess( - objectMapper.writeValueAsString(accessTokenResponse), + objectMapper.writeValueAsString(유효한_액세스_토큰), MediaType.APPLICATION_JSON )); mockRestServiceServer .expect(requestTo(matchesPattern( - "https://sgisapi.kostat.go.kr/OpenAPI3/addr/stage.json.*" + 단계별_지역_목록_조회_URI_패턴 ))) .andRespond(withSuccess( objectMapper.writeValueAsString(null), diff --git a/backend/ddang/src/test/java/com/ddang/ddang/region/infrastructure/api/fixture/RestTemplateInitRegionProcessorFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/region/infrastructure/api/fixture/RestTemplateInitRegionProcessorFixture.java new file mode 100644 index 000000000..ad1616be8 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/region/infrastructure/api/fixture/RestTemplateInitRegionProcessorFixture.java @@ -0,0 +1,36 @@ +package com.ddang.ddang.region.infrastructure.api.fixture; + +import com.ddang.ddang.region.infrastructure.api.dto.ApiAccessTokenResponse; +import com.ddang.ddang.region.infrastructure.api.dto.ResultApiRegionResponse; +import com.ddang.ddang.region.infrastructure.api.dto.TotalApiRegionResponse; + +import java.util.Collections; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class RestTemplateInitRegionProcessorFixture { + + private String 액세스_토큰 = "accessToken"; + private String 서울특별시_지역_코드 = "11"; + private String 강남구_지역_코드 = "11230"; + private String 개포1동_지역_코드 = "11230680"; + private String 단계별_지역_목록_조회_URI = "https://sgisapi.kostat.go.kr/OpenAPI3/addr/stage.json"; + + protected String 액세스_토큰_요청_URI_패턴 = "https://sgisapi.kostat.go.kr/OpenAPI3/auth/authentication.json.*"; + protected String 단계별_지역_목록_조회_URI_패턴 = 단계별_지역_목록_조회_URI + ".*"; + protected String 첫번쪠_지역_목록_조회_URI = 단계별_지역_목록_조회_URI + "?accessToken=" + 액세스_토큰; + protected String 서울특별시_하위_지역_목록_조회_URI = + 단계별_지역_목록_조회_URI + "?accessToken=" + 액세스_토큰 + "&cd=" + 서울특별시_지역_코드; + protected String 서울특별시_강남구_하위_지역_목록_조회_URI = + 단계별_지역_목록_조회_URI + "?accessToken=" + 액세스_토큰 + "&cd=" + 강남구_지역_코드; + protected ApiAccessTokenResponse 유효한_액세스_토큰 = new ApiAccessTokenResponse( + Collections.singletonMap("accessToken", 액세스_토큰) + ); + protected ApiAccessTokenResponse 유효하지_않은_액세스_토큰 = new ApiAccessTokenResponse(null); + protected ResultApiRegionResponse 서울특별시 = new ResultApiRegionResponse("서울특별시", 서울특별시_지역_코드); + protected ResultApiRegionResponse 강남구 = new ResultApiRegionResponse("강남구", 강남구_지역_코드); + protected ResultApiRegionResponse 개포1동 = new ResultApiRegionResponse("개포1동", 개포1동_지역_코드); + protected TotalApiRegionResponse 첫번째_지역_목록_조회_응답 = new TotalApiRegionResponse(List.of(서울특별시)); + protected TotalApiRegionResponse 서울특별시_하위_지역_목록_조회_응답 = new TotalApiRegionResponse(List.of(강남구)); + protected TotalApiRegionResponse 서울특별시_강남구의_하위_지역_목록_조회_응답 = new TotalApiRegionResponse(List.of(개포1동)); +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/region/infrastructure/persistence/JpaRegionRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/region/infrastructure/persistence/JpaRegionRepositoryTest.java index 6330019ba..2461e0ac3 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/region/infrastructure/persistence/JpaRegionRepositoryTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/region/infrastructure/persistence/JpaRegionRepositoryTest.java @@ -1,13 +1,8 @@ package com.ddang.ddang.region.infrastructure.persistence; -import static org.assertj.core.api.Assertions.assertThat; - import com.ddang.ddang.configuration.QuerydslConfiguration; import com.ddang.ddang.region.domain.Region; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import java.util.List; -import java.util.Optional; +import com.ddang.ddang.region.infrastructure.persistence.fixture.JpaRegionRepositoryFixture; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; @@ -16,132 +11,96 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + @DataJpaTest +@Import(QuerydslConfiguration.class) @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -@Import(QuerydslConfiguration.class) -class JpaRegionRepositoryTest { - - @PersistenceContext - EntityManager em; +class JpaRegionRepositoryTest extends JpaRegionRepositoryFixture { @Autowired JpaRegionRepository regionRepository; @Test void 모든_첫번째_지역을_조회한다() { - // given - final Region first1 = new Region("first1"); - final Region first2 = new Region("first2"); - final Region second = new Region("second"); - - first1.addSecondRegion(second); - - regionRepository.save(first1); - regionRepository.save(first2); - - em.flush(); - em.clear(); - // when final List actual = regionRepository.findFirstAll(); // then - assertThat(actual).hasSize(2); + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(2); + softAssertions.assertThat(actual.get(0)).isEqualTo(서울특별시); + softAssertions.assertThat(actual.get(1)).isEqualTo(경기도); + }); } @Test void 첫번째_지역에_해당하는_모든_두번째_지역을_조회한다() { - // given - final Region first = new Region("first"); - final Region second1 = new Region("second1"); - final Region second2 = new Region("second2"); - - first.addSecondRegion(second1); - first.addSecondRegion(second2); - - regionRepository.save(first); - - em.flush(); - em.clear(); - // when - final List actual = regionRepository.findSecondAllByFirstRegionId(first.getId()); + final List actual = regionRepository.findSecondAllByFirstRegionId(서울특별시.getId()); // then - assertThat(actual).hasSize(2); + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(2); + softAssertions.assertThat(actual.get(0)).isEqualTo(서울특별시_강남구); + softAssertions.assertThat(actual.get(1)).isEqualTo(서울특별시_송파구); + }); } @Test void 두번째_지역에_해당하는_모든_세번째_지역을_조회한다() { - // given - final Region first = new Region("first"); - final Region second = new Region("second"); - final Region third1 = new Region("third1"); - final Region third2 = new Region("third2"); - - first.addSecondRegion(second); - second.addThirdRegion(third1); - second.addThirdRegion(third2); - - regionRepository.save(first); - - em.flush(); - em.clear(); - // when - final List actual = regionRepository.findThirdAllByFirstAndSecondRegionId(first.getId(), second.getId()); + final List actual = + regionRepository.findThirdAllByFirstAndSecondRegionId(서울특별시.getId(), 서울특별시_강남구.getId()); // then - assertThat(actual).hasSize(2); + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(2); + softAssertions.assertThat(actual.get(0)).isEqualTo(서울특별시_강남구_삼성동); + softAssertions.assertThat(actual.get(1)).isEqualTo(서울특별시_강남구_대치동); + }); } @Test void 세번째_지역을_조회한다() { - // given - final Region first = new Region("first"); - final Region second = new Region("second"); - final Region third1 = new Region("third1"); - final Region third2 = new Region("third2"); + // when + final Optional actual = regionRepository.findThirdRegionById(서울특별시_강남구_삼성동.getId()); - first.addSecondRegion(second); - second.addThirdRegion(third1); - second.addThirdRegion(third2); + // then + assertThat(actual).contains(서울특별시_강남구_삼성동); + } - regionRepository.save(first); + @Test + void 세번째_지역이_아닌_아이디를_전달하면_빈_Optional을_반환한다() { + // when + final Optional actual = regionRepository.findThirdRegionById(서울특별시.getId()); - em.flush(); - em.clear(); + // then + assertThat(actual).isEmpty(); + } + @Test + void 세번째_지역에_해당하는_모든_id를_전달하면_그에_맞는_thirdRegions를_반환한다() { // when - final Optional actual = regionRepository.findThirdRegionById(third1.getId()); + final List thirdRegionIds = List.of(서울특별시_강남구_삼성동.getId(), 서울특별시_강남구_대치동.getId()); + final List actual = regionRepository.findAllThirdRegionByIds(thirdRegionIds); // then SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual).isPresent(); - softAssertions.assertThat(actual.get()).isEqualTo(third1); + softAssertions.assertThat(actual).hasSize(2); + softAssertions.assertThat(actual.get(0)).isEqualTo(서울특별시_강남구_삼성동); + softAssertions.assertThat(actual.get(1)).isEqualTo(서울특별시_강남구_대치동); }); } @Test - void 세번째_지역이_아닌_아이디를_전달하면_빈_Optional을_반환한다() { - // given - final Region first = new Region("first"); - final Region second = new Region("second"); - final Region third1 = new Region("third1"); - final Region third2 = new Region("third2"); - - first.addSecondRegion(second); - second.addThirdRegion(third1); - second.addThirdRegion(third2); - - regionRepository.save(first); - - em.flush(); - em.clear(); - + void 세번째_지역이_아닌_아이디를_전달하면_빈_List를_반환한다() { // when - final Optional actual = regionRepository.findThirdRegionById(first.getId()); + final List actual = regionRepository.findAllThirdRegionByIds(List.of(서울특별시.getId())); // then assertThat(actual).isEmpty(); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/region/infrastructure/persistence/fixture/JpaRegionRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/region/infrastructure/persistence/fixture/JpaRegionRepositoryFixture.java new file mode 100644 index 000000000..cb50b68b1 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/region/infrastructure/persistence/fixture/JpaRegionRepositoryFixture.java @@ -0,0 +1,47 @@ +package com.ddang.ddang.region.infrastructure.persistence.fixture; + +import com.ddang.ddang.region.domain.Region; +import com.ddang.ddang.region.infrastructure.persistence.JpaRegionRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +@SuppressWarnings("NonAsciiCharacters") +public class JpaRegionRepositoryFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaRegionRepository regionRepository; + + protected Region 서울특별시; + protected Region 경기도; + protected Region 서울특별시_강남구; + protected Region 서울특별시_송파구; + protected Region 서울특별시_강남구_삼성동; + protected Region 서울특별시_강남구_대치동; + + @BeforeEach + void setUp() { + 서울특별시 = new Region("서울특별시"); + 경기도 = new Region("경기도"); + 서울특별시_강남구 = new Region("강남구"); + 서울특별시_송파구 = new Region("송파구"); + 서울특별시_강남구_삼성동 = new Region("삼성동"); + 서울특별시_강남구_대치동 = new Region("대치동"); + + 서울특별시.addSecondRegion(서울특별시_강남구); + 서울특별시.addSecondRegion(서울특별시_송파구); + + 서울특별시_강남구.addThirdRegion(서울특별시_강남구_삼성동); + 서울특별시_강남구.addThirdRegion(서울특별시_강남구_대치동); + + regionRepository.save(서울특별시); + regionRepository.save(경기도); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/region/presentation/RegionControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/region/presentation/RegionControllerTest.java index 84a2ac223..9c8a1b9e1 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/region/presentation/RegionControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/region/presentation/RegionControllerTest.java @@ -1,68 +1,38 @@ package com.ddang.ddang.region.presentation; -import static org.hamcrest.Matchers.is; -import static org.mockito.BDDMockito.given; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.ddang.ddang.configuration.RestDocsConfiguration; import com.ddang.ddang.exception.GlobalExceptionHandler; -import com.ddang.ddang.region.application.RegionService; -import com.ddang.ddang.region.application.dto.ReadRegionDto; import com.ddang.ddang.region.application.exception.RegionNotFoundException; -import java.util.List; +import com.ddang.ddang.region.presentation.fixture.RegionControllerFixture; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; -import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@WebMvcTest(controllers = {RegionController.class}, - excludeFilters = { - @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfigurer.class), - @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com\\.ddang\\.ddang\\.authentication\\.configuration\\..*") - } -) -@AutoConfigureRestDocs -@Import(RestDocsConfiguration.class) -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -@SuppressWarnings("NonAsciiCharacters") -class RegionControllerTest { - @MockBean - RegionService regionService; +import java.util.List; - @Autowired - RegionController regionController; +import static org.hamcrest.Matchers.is; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @Autowired - RestDocumentationResultHandler restDocs; +@SuppressWarnings("NonAsciiCharacters") +class RegionControllerTest extends RegionControllerFixture { MockMvc mockMvc; @BeforeEach - void setUp(@Autowired RestDocumentationContextProvider provider) { + void setUp() { mockMvc = MockMvcBuilders.standaloneSetup(regionController) .setControllerAdvice(new GlobalExceptionHandler()) .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) @@ -74,154 +44,160 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { @Test void 모든_첫번째_지역을_조회한다() throws Exception { // given - - final ReadRegionDto first1 = new ReadRegionDto(1L, "서울특별시"); - final ReadRegionDto first2 = new ReadRegionDto(2L, "부산광역시"); - - given(regionService.readAllFirst()).willReturn(List.of(first1, first2)); + given(regionService.readAllFirst()).willReturn(List.of(서울특별시, 부산광역시)); // when & then - mockMvc.perform(get("/regions") - .contentType(MediaType.APPLICATION_JSON)) - .andExpectAll( - status().isOk(), - jsonPath("$.[0].id", is(first1.id()), Long.class), - jsonPath("$.[0].name", is(first1.name())), - jsonPath("$.[1].id", is(first2.id()), Long.class), - jsonPath("$.[1].name", is(first2.name())) - ) - .andDo( - restDocs.document( - responseFields( - fieldWithPath("[]").type(JsonFieldType.ARRAY).description("모든 첫 번째 직거래 지역"), - fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("첫 번째 직거래 지역 ID"), - fieldWithPath("[].name").type(JsonFieldType.STRING).description("첫 번째 직거래 지역 이름") - ) - ) - ); + final ResultActions resultActions = mockMvc.perform(get("/regions") + .contentType(MediaType.APPLICATION_JSON)) + .andExpectAll( + status().isOk(), + jsonPath("$.[0].id", is(서울특별시.id()), Long.class), + jsonPath("$.[0].name", is(서울특별시.name())), + jsonPath("$.[1].id", is(부산광역시.id()), Long.class), + jsonPath("$.[1].name", is(부산광역시.name())) + ); + + readAllFirst_문서화(resultActions); } @Test void 첫번째_지역이_없는_경우_첫번째_지역_조회시_404를_반환한다() throws Exception { // given - final RegionNotFoundException regionNotFoundException = new RegionNotFoundException("등록된 지역이 없습니다."); - given(regionService.readAllFirst()).willThrow(regionNotFoundException); + given(regionService.readAllFirst()).willThrow(new RegionNotFoundException("등록된 지역이 없습니다.")); // when & then mockMvc.perform(get("/regions") .contentType(MediaType.APPLICATION_JSON)) .andExpectAll( status().isNotFound(), - jsonPath("$.message", is(regionNotFoundException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 첫번째_지역에_해당하는_모든_두번째_지역을_조회한다() throws Exception { // given - final ReadRegionDto first = new ReadRegionDto(1L, "서울특별시"); - final ReadRegionDto second1 = new ReadRegionDto(2L, "강남구"); - final ReadRegionDto second2 = new ReadRegionDto(3L, "강동구"); - - given(regionService.readAllSecondByFirstRegionId(first.id())).willReturn(List.of(second1, second2)); + given(regionService.readAllSecondByFirstRegionId(서울특별시.id())).willReturn(List.of(서울특별시_하위_강남구, 서울특별시_하위_강동구)); // when & then - mockMvc.perform(RestDocumentationRequestBuilders.get("/regions/{firstId}", first.id()) - .contentType(MediaType.APPLICATION_JSON)) - .andExpectAll( - status().isOk(), - jsonPath("$.[0].id", is(second1.id()), Long.class), - jsonPath("$.[0].name", is(second1.name())), - jsonPath("$.[1].id", is(second2.id()), Long.class), - jsonPath("$.[1].name", is(second2.name())) - ) - .andDo( - restDocs.document( - pathParameters( - parameterWithName("firstId").description("첫 번째 지역 ID") - ), - responseFields( - fieldWithPath("[]").type(JsonFieldType.ARRAY).description("첫 번째 지역에 해당하는 모든 두 번째 직거래 지역"), - fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("두 번째 직거래 지역 ID"), - fieldWithPath("[].name").type(JsonFieldType.STRING).description("두 번째 직거래 지역 이름") - ) - ) - ); + final ResultActions resultActions = + mockMvc.perform(RestDocumentationRequestBuilders.get("/regions/{firstId}", 서울특별시.id()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpectAll( + status().isOk(), + jsonPath("$.[0].id", is(서울특별시_하위_강남구.id()), Long.class), + jsonPath("$.[0].name", is(서울특별시_하위_강남구.name())), + jsonPath("$.[1].id", is(서울특별시_하위_강동구.id()), Long.class), + jsonPath("$.[1].name", is(서울특별시_하위_강동구.name())) + ); + + readAllSecond_문서화(resultActions); } @Test void 지정한_첫번째_지역에_해당하는_두번째_지역이_없는_경우_두번째_지역_조회시_404를_반환한다() throws Exception { // given - final ReadRegionDto first = new ReadRegionDto(1L, "first"); - - final RegionNotFoundException regionNotFoundException = - new RegionNotFoundException("지정한 첫 번째 지역에 해당하는 두 번째 지역이 없습니다."); - - given(regionService.readAllSecondByFirstRegionId(first.id())).willThrow(regionNotFoundException); + given(regionService.readAllSecondByFirstRegionId(두번째_지역이_없는_첫번째_지역.id())) + .willThrow(new RegionNotFoundException("지정한 첫 번째 지역에 해당하는 두 번째 지역이 없습니다.")); // when & then - mockMvc.perform(get("/regions/{firstId}", first.id()) + mockMvc.perform(get("/regions/{firstId}", 두번째_지역이_없는_첫번째_지역.id()) .contentType(MediaType.APPLICATION_JSON)) .andExpectAll( status().isNotFound(), - jsonPath("$.message", is(regionNotFoundException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 두번째_지역에_해당하는_모든_세번째_지역을_조회한다() throws Exception { // given - final ReadRegionDto first = new ReadRegionDto(1L, "서울특별시"); - final ReadRegionDto second = new ReadRegionDto(2L, "강남구"); - final ReadRegionDto third1 = new ReadRegionDto(3L, "개포1동"); - final ReadRegionDto third2 = new ReadRegionDto(4L, "개포2동"); - - given(regionService.readAllThirdByFirstAndSecondRegionId(first.id(), second.id())) - .willReturn(List.of(third1, third2)); + given(regionService.readAllThirdByFirstAndSecondRegionId(서울특별시.id(), 서울특별시_하위_강남구.id())) + .willReturn(List.of(서울특별시_하위_강남구_하위_개포1동, 서울특별시_하위_강남구_하위_개포2동)); // when & then - mockMvc.perform(RestDocumentationRequestBuilders.get("/regions/{firstId}/{secondId}", first.id(), second.id()) - .contentType(MediaType.APPLICATION_JSON)) - .andExpectAll( - status().isOk(), - jsonPath("$.[0].id", is(third1.id()), Long.class), - jsonPath("$.[0].name", is(third1.name())), - jsonPath("$.[1].id", is(third2.id()), Long.class), - jsonPath("$.[1].name", is(third2.name())) - ) - .andDo( - restDocs.document( - pathParameters( - parameterWithName("firstId").description("첫 번째 지역 ID"), - parameterWithName("secondId").description("두 번째 지역 ID") - ), - responseFields( - fieldWithPath("[]").type(JsonFieldType.ARRAY).description("두 번째 지역에 해당하는 모든 세 번째 직거래 지역"), - fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("세 번째 직거래 지역 ID"), - fieldWithPath("[].name").type(JsonFieldType.STRING).description("세 번째 직거래 지역 이름") - ) - ) - ); + final ResultActions resultActions = + mockMvc.perform(RestDocumentationRequestBuilders.get("/regions/{firstId}/{secondId}", + 서울특별시.id(), + 서울특별시_하위_강남구.id()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpectAll( + status().isOk(), + jsonPath("$.[0].id", is(서울특별시_하위_강남구_하위_개포1동.id()), Long.class), + jsonPath("$.[0].name", is(서울특별시_하위_강남구_하위_개포1동.name())), + jsonPath("$.[1].id", is(서울특별시_하위_강남구_하위_개포2동.id()), Long.class), + jsonPath("$.[1].name", is(서울특별시_하위_강남구_하위_개포2동.name())) + ); + + readAllThird_문서화(resultActions); } @Test void 지정한_첫번째와_두번째_지역에_해당하는_세번째_지역이_없는_경우_세번째_지역_조회시_404를_반환한다() throws Exception { // given - final ReadRegionDto first = new ReadRegionDto(1L, "first"); - final ReadRegionDto second = new ReadRegionDto(2L, "second"); - - final RegionNotFoundException regionNotFoundException = - new RegionNotFoundException("지정한 첫 번째와 두 번째 지역에 해당하는 세 번째 지역이 없습니다."); - - given(regionService.readAllThirdByFirstAndSecondRegionId(first.id(), second.id())) - .willThrow(regionNotFoundException); + given(regionService.readAllThirdByFirstAndSecondRegionId(서울특별시.id(), 세번째_지역이_없는_두번째_지역.id())) + .willThrow(new RegionNotFoundException("지정한 첫 번째와 두 번째 지역에 해당하는 세 번째 지역이 없습니다.")); // when & then - mockMvc.perform(get("/regions/{firstId}/{secondId}", first.id(), second.id()) + mockMvc.perform(get("/regions/{firstId}/{secondId}", 서울특별시.id(), 세번째_지역이_없는_두번째_지역.id()) .contentType(MediaType.APPLICATION_JSON)) .andExpectAll( status().isNotFound(), - jsonPath("$.message", is(regionNotFoundException.getMessage())) + jsonPath("$.message").exists() ); } + + private void readAllFirst_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + responseFields( + fieldWithPath("[]").type(JsonFieldType.ARRAY) + .description("모든 첫 번째 직거래 지역"), + fieldWithPath("[].id").type(JsonFieldType.NUMBER) + .description("첫 번째 직거래 지역 ID"), + fieldWithPath("[].name").type(JsonFieldType.STRING) + .description("첫 번째 직거래 지역 이름") + ) + ) + ); + } + + private void readAllSecond_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + pathParameters( + parameterWithName("firstId").description("첫 번째 지역 ID") + ), + responseFields( + fieldWithPath("[]").type(JsonFieldType.ARRAY) + .description("첫 번째 지역에 해당하는 모든 두 번째 직거래 지역"), + fieldWithPath("[].id").type(JsonFieldType.NUMBER) + .description("두 번째 직거래 지역 ID"), + fieldWithPath("[].name").type(JsonFieldType.STRING) + .description("두 번째 직거래 지역 이름") + ) + ) + ); + } + + private void readAllThird_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + pathParameters( + parameterWithName("firstId") + .description("첫 번째 지역 ID"), + parameterWithName("secondId") + .description("두 번째 지역 ID") + ), + responseFields( + fieldWithPath("[]").type(JsonFieldType.ARRAY) + .description("두 번째 지역에 해당하는 모든 세 번째 직거래 지역"), + fieldWithPath("[].id").type(JsonFieldType.NUMBER) + .description("세 번째 직거래 지역 ID"), + fieldWithPath("[].name").type(JsonFieldType.STRING) + .description("세 번째 직거래 지역 이름") + ) + ) + ); + } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/region/presentation/fixture/RegionControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/region/presentation/fixture/RegionControllerFixture.java new file mode 100644 index 000000000..49c4430ee --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/region/presentation/fixture/RegionControllerFixture.java @@ -0,0 +1,17 @@ +package com.ddang.ddang.region.presentation.fixture; + +import com.ddang.ddang.configuration.CommonControllerSliceTest; +import com.ddang.ddang.region.application.dto.ReadRegionDto; + +@SuppressWarnings("NonAsciiCharacters") +public class RegionControllerFixture extends CommonControllerSliceTest { + + protected ReadRegionDto 서울특별시 = new ReadRegionDto(1L, "서울특별시"); + protected ReadRegionDto 부산광역시 = new ReadRegionDto(2L, "부산광역시"); + protected ReadRegionDto 두번째_지역이_없는_첫번째_지역 = new ReadRegionDto(3L, "두번째 지역이 없는 첫번째 지역"); + protected ReadRegionDto 서울특별시_하위_강남구 = new ReadRegionDto(4L, "강남구"); + protected ReadRegionDto 서울특별시_하위_강동구 = new ReadRegionDto(5L, "강동구"); + protected ReadRegionDto 세번째_지역이_없는_두번째_지역 = new ReadRegionDto(6L, "세번째 지역이 없는 두번째 지역"); + protected ReadRegionDto 서울특별시_하위_강남구_하위_개포1동 = new ReadRegionDto(7L, "개포1동"); + protected ReadRegionDto 서울특별시_하위_강남구_하위_개포2동 = new ReadRegionDto(8L, "개포2동"); +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/report/application/AnswerReportServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/report/application/AnswerReportServiceTest.java new file mode 100644 index 000000000..9b0971c34 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/report/application/AnswerReportServiceTest.java @@ -0,0 +1,85 @@ +package com.ddang.ddang.report.application; + +import com.ddang.ddang.configuration.IsolateDatabase; +import com.ddang.ddang.qna.application.exception.AnswerNotFoundException; +import com.ddang.ddang.report.application.dto.ReadAnswerReportDto; +import com.ddang.ddang.report.application.exception.InvalidAnswererReportException; +import com.ddang.ddang.report.application.fixture.AnswerReportServiceFixture; +import com.ddang.ddang.user.application.exception.UserNotFoundException; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@IsolateDatabase +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AnswerReportServiceTest extends AnswerReportServiceFixture { + + @Autowired + AnswerReportService answerReportService; + + @Test + void 답변_신고를_등록한다() { + // when + final Long actual = answerReportService.create(답변_신고_요청_dto); + + // then + assertThat(actual).isPositive(); + } + + @Test + void 존재하지_않는_답변_신고시_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> answerReportService.create(존재하지_않는_답변_신고_요청_dto)) + .isInstanceOf(AnswerNotFoundException.class) + .hasMessage("해당 답변을 찾을 수 없습니다."); + } + + @Test + void 존재하지_않는_사용자가_답변_신고시_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> answerReportService.create(존재하지_않는_사용자가_답변_신고_요청_dto)) + .isInstanceOf(UserNotFoundException.class) + .hasMessage("해당 사용자를 찾을 수 없습니다."); + } + + @Test + void 답변자가_본인_답변_신고시_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> answerReportService.create(답변자가_본인_답변_신고_요청_dto)) + .isInstanceOf(InvalidAnswererReportException.class) + .hasMessage("본인 답변입니다."); + } + + @Test + void 이미_신고한_답변_신고시_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> answerReportService.create(이미_신고한_답변_신고_요청_dto)) + .isInstanceOf(InvalidAnswererReportException.class) + .hasMessage("이미 신고한 답변입니다."); + } + + @Test + void 전체_신고_목록을_조회한다() { + // when + final List actual = answerReportService.readAll(); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.get(0).reporterDto().id()).isEqualTo(이미_신고한_신고자1.getId()); + softAssertions.assertThat(actual.get(0).id()).isEqualTo(답변_신고1.getId()); + softAssertions.assertThat(actual.get(1).reporterDto().id()).isEqualTo(이미_신고한_신고자2.getId()); + softAssertions.assertThat(actual.get(1).id()).isEqualTo(답변_신고2.getId()); + softAssertions.assertThat(actual.get(2).reporterDto().id()).isEqualTo(이미_신고한_신고자3.getId()); + softAssertions.assertThat(actual.get(2).id()).isEqualTo(답변_신고3.getId()); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/report/application/AuctionReportServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/report/application/AuctionReportServiceTest.java index 087616571..e754fa7a6 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/report/application/AuctionReportServiceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/report/application/AuctionReportServiceTest.java @@ -1,26 +1,19 @@ package com.ddang.ddang.report.application; import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; -import com.ddang.ddang.auction.domain.Auction; -import com.ddang.ddang.auction.domain.BidUnit; -import com.ddang.ddang.auction.domain.Price; -import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; import com.ddang.ddang.configuration.IsolateDatabase; -import com.ddang.ddang.report.application.dto.CreateAuctionReportDto; import com.ddang.ddang.report.application.dto.ReadAuctionReportDto; import com.ddang.ddang.report.application.exception.AlreadyReportAuctionException; import com.ddang.ddang.report.application.exception.InvalidReportAuctionException; import com.ddang.ddang.report.application.exception.InvalidReporterToAuctionException; +import com.ddang.ddang.report.application.fixture.AuctionReportServiceFixture; import com.ddang.ddang.user.application.exception.UserNotFoundException; -import com.ddang.ddang.user.domain.User; -import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import java.time.LocalDateTime; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -29,52 +22,15 @@ @IsolateDatabase @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class AuctionReportServiceTest { +class AuctionReportServiceTest extends AuctionReportServiceFixture { @Autowired AuctionReportService auctionReportService; - @Autowired - JpaAuctionRepository auctionRepository; - - @Autowired - JpaUserRepository userRepository; - @Test void 경매_신고를_등록한다() { - // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User user = User.builder() - .name("사용자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - - userRepository.save(seller); - auctionRepository.save(auction); - userRepository.save(user); - - final CreateAuctionReportDto createAuctionReportDto = new CreateAuctionReportDto( - auction.getId(), - "신고합니다", - user.getId()); - // when - final Long actual = auctionReportService.create(createAuctionReportDto); + final Long actual = auctionReportService.create(새로운_경매_신고_요청_dto); // then assertThat(actual).isPositive(); @@ -82,253 +38,58 @@ class AuctionReportServiceTest { @Test void 존재하지_않는_사용자가_신고하는_경우_예외가_발생한다() { - // given - final Long invalidUserId = -9999L; - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - - userRepository.save(seller); - auctionRepository.save(auction); - - final CreateAuctionReportDto createAuctionReportDto = new CreateAuctionReportDto( - auction.getId(), - "신고합니다", - invalidUserId - ); - // when & then - assertThatThrownBy(() -> auctionReportService.create(createAuctionReportDto)) + assertThatThrownBy(() -> auctionReportService.create(존재하지_않는_사용자의_경매_신고_요청_dto)) .isInstanceOf(UserNotFoundException.class) .hasMessage("해당 사용자를 찾을 수 없습니다."); } @Test void 존재하지_않는_경매를_신고하는_경우_예외가_발생한다() { - final Long invalidAuctionId = -9999L; - final User user = User.builder() - .name("사용자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(user); - - final CreateAuctionReportDto createAuctionReportDto = new CreateAuctionReportDto( - invalidAuctionId, - "신고합니다", - user.getId() - ); - // when & then - assertThatThrownBy(() -> auctionReportService.create(createAuctionReportDto)) + assertThatThrownBy(() -> auctionReportService.create(존재하지_않는_경매_신고_요청_dto)) .isInstanceOf(AuctionNotFoundException.class) .hasMessage("해당 경매를 찾을 수 없습니다."); } @Test void 본인이_등록한_경매를_신고하는_경우_예외가_발생한다() { - // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - - userRepository.save(seller); - auctionRepository.save(auction); - - final CreateAuctionReportDto createAuctionReportDto = new CreateAuctionReportDto( - auction.getId(), - "신고합니다", - seller.getId() - ); - // when & then - assertThatThrownBy(() -> auctionReportService.create(createAuctionReportDto)) + assertThatThrownBy(() -> auctionReportService.create(판매자가_본인의_경매_신고_요청_dto)) .isInstanceOf(InvalidReporterToAuctionException.class) .hasMessage("본인 경매글입니다."); } @Test void 삭제한_경매를_신고하는_경우_예외가_발생한다() { - // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User user = User.builder() - .name("사용자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - - userRepository.save(seller); - auctionRepository.save(auction); - userRepository.save(user); - - auction.delete(); - - final CreateAuctionReportDto createAuctionReportDto = new CreateAuctionReportDto( - auction.getId(), - "신고합니다", - user.getId() - ); - // when & then - assertThatThrownBy(() -> auctionReportService.create(createAuctionReportDto)) + assertThatThrownBy(() -> auctionReportService.create(삭제된_경매_신고_요청_dto)) .isInstanceOf(InvalidReportAuctionException.class) .hasMessage("이미 삭제된 경매입니다."); } @Test void 이미_신고한_경매를_동일_사용자가_신고하는_경우_예외가_발생한다() { - // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User user = User.builder() - .name("사용자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - - userRepository.save(seller); - auctionRepository.save(auction); - userRepository.save(user); - - final CreateAuctionReportDto createAuctionReportDto = new CreateAuctionReportDto( - auction.getId(), - "신고합니다", - user.getId() - ); - auctionReportService.create(createAuctionReportDto); - // when & then - assertThatThrownBy(() -> auctionReportService.create(createAuctionReportDto)) + assertThatThrownBy(() -> auctionReportService.create(이미_신고한_사용자가_경매_신고_요청_dto)) .isInstanceOf(AlreadyReportAuctionException.class) .hasMessage("이미 신고한 경매입니다."); } @Test void 전체_신고_목록을_조회한다() { - // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User user1 = User.builder() - .name("사용자1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - final User user2 = User.builder() - .name("사용자2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12347") - .build(); - final User user3 = User.builder() - .name("사용자3") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12348") - .build(); - - userRepository.save(seller); - auctionRepository.save(auction); - userRepository.save(user1); - userRepository.save(user2); - userRepository.save(user3); - - final CreateAuctionReportDto createAuctionReportDto1 = new CreateAuctionReportDto( - auction.getId(), - "신고합니다", - user1.getId() - ); - auctionReportService.create(createAuctionReportDto1); - - final CreateAuctionReportDto createAuctionReportDto2 = new CreateAuctionReportDto( - auction.getId(), - "신고합니다", - user2.getId() - ); - auctionReportService.create(createAuctionReportDto2); - - final CreateAuctionReportDto createAuctionReportDto3 = new CreateAuctionReportDto( - auction.getId(), - "신고합니다", - user3.getId() - ); - auctionReportService.create(createAuctionReportDto3); - - // when final List actual = auctionReportService.readAll(); // then SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual.get(0).reporterDto().id()).isEqualTo(user1.getId()); - softAssertions.assertThat(actual.get(0).auctionDto().id()).isEqualTo(auction.getId()); - softAssertions.assertThat(actual.get(1).reporterDto().id()).isEqualTo(user2.getId()); - softAssertions.assertThat(actual.get(1).auctionDto().id()).isEqualTo(auction.getId()); - softAssertions.assertThat(actual.get(2).reporterDto().id()).isEqualTo(user3.getId()); - softAssertions.assertThat(actual.get(2).auctionDto().id()).isEqualTo(auction.getId()); + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.get(0).reporterDto().id()).isEqualTo(이미_신고한_신고자1.getId()); + softAssertions.assertThat(actual.get(0).auctionDto().id()).isEqualTo(경매.getId()); + softAssertions.assertThat(actual.get(1).reporterDto().id()).isEqualTo(이미_신고한_신고자2.getId()); + softAssertions.assertThat(actual.get(1).auctionDto().id()).isEqualTo(경매.getId()); + softAssertions.assertThat(actual.get(2).reporterDto().id()).isEqualTo(이미_신고한_신고자3.getId()); + softAssertions.assertThat(actual.get(2).auctionDto().id()).isEqualTo(경매.getId()); }); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/report/application/ChatRoomReportServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/report/application/ChatRoomReportServiceTest.java index 288e7f794..b77a72ef2 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/report/application/ChatRoomReportServiceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/report/application/ChatRoomReportServiceTest.java @@ -1,28 +1,18 @@ package com.ddang.ddang.report.application; -import com.ddang.ddang.auction.domain.Auction; -import com.ddang.ddang.auction.domain.BidUnit; -import com.ddang.ddang.auction.domain.Price; -import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; import com.ddang.ddang.chat.application.exception.ChatRoomNotFoundException; -import com.ddang.ddang.chat.domain.ChatRoom; -import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; import com.ddang.ddang.configuration.IsolateDatabase; -import com.ddang.ddang.report.application.dto.CreateChatRoomReportDto; import com.ddang.ddang.report.application.dto.ReadChatRoomReportDto; import com.ddang.ddang.report.application.exception.AlreadyReportChatRoomException; -import com.ddang.ddang.report.application.exception.ChatRoomReportNotAccessibleException; +import com.ddang.ddang.report.application.exception.InvalidChatRoomReportException; +import com.ddang.ddang.report.application.fixture.ChatRoomReportServiceFixture; import com.ddang.ddang.user.application.exception.UserNotFoundException; -import com.ddang.ddang.user.domain.User; -import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; -import jakarta.persistence.EntityManager; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import java.time.LocalDateTime; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -31,61 +21,15 @@ @IsolateDatabase @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class ChatRoomReportServiceTest { - - @Autowired - EntityManager em; +class ChatRoomReportServiceTest extends ChatRoomReportServiceFixture { @Autowired ChatRoomReportService chatRoomReportService; - @Autowired - JpaUserRepository userRepository; - - @Autowired - JpaAuctionRepository auctionRepository; - - @Autowired - JpaChatRoomRepository chatRoomRepository; - @Test void 채팅방_신고를_등록한다() { - // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User buyer = User.builder() - .name("구매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - final ChatRoom chatRoom = new ChatRoom(auction, buyer); - - userRepository.save(seller); - auctionRepository.save(auction); - userRepository.save(buyer); - chatRoomRepository.save(chatRoom); - - final CreateChatRoomReportDto createChatRoomReportDto = new CreateChatRoomReportDto( - chatRoom.getId(), - "신고합니다.", - buyer.getId() - ); - // when - final Long actual = chatRoomReportService.create(createChatRoomReportDto); + final Long actual = chatRoomReportService.create(채팅방_신고_요청_dto); // then assertThat(actual).isPositive(); @@ -93,290 +37,50 @@ class ChatRoomReportServiceTest { @Test void 존재하지_않는_사용자가_채팅방을_신고할시_예외가_발생한다() { - // given - final Long invalidUserId = -999L; - - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User buyer = User.builder() - .name("구매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - final ChatRoom chatRoom = new ChatRoom(auction, buyer); - - userRepository.save(seller); - auctionRepository.save(auction); - userRepository.save(buyer); - chatRoomRepository.save(chatRoom); - - final CreateChatRoomReportDto createChatRoomReportDto = new CreateChatRoomReportDto( - chatRoom.getId(), - "신고합니다.", - invalidUserId - ); - // when & then - assertThatThrownBy(() -> chatRoomReportService.create(createChatRoomReportDto)) + assertThatThrownBy(() -> chatRoomReportService.create(존재하지_않는_사용자의_채팅방_신고_요청_dto)) .isInstanceOf(UserNotFoundException.class) .hasMessage("해당 사용자를 찾을 수 없습니다."); } @Test void 존재하지_않는_채팅방을_신고할시_예외가_발생한다() { - // given - final Long invalidChatRoomId = -999L; - - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User buyer = User.builder() - .name("구매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - - userRepository.save(seller); - userRepository.save(buyer); - auctionRepository.save(auction); - - final User invalidReporter = User.builder() - .name("사용자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12347") - .build(); - userRepository.save(invalidReporter); - - final CreateChatRoomReportDto createChatRoomReportDto = new CreateChatRoomReportDto( - invalidChatRoomId, - "신고합니다.", - invalidReporter.getId() - ); - // when & then - assertThatThrownBy(() -> chatRoomReportService.create(createChatRoomReportDto)) + assertThatThrownBy(() -> chatRoomReportService.create(존재하지_않는_채팅방_신고_요청_dto)) .isInstanceOf(ChatRoomNotFoundException.class) .hasMessage("해당 채팅방을 찾을 수 없습니다."); } @Test void 판매자와_구매자_외의_사용자가_채팅방을_신고할시_예외가_발생한다() { - // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User buyer = User.builder() - .name("구매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - final ChatRoom chatRoom = new ChatRoom(auction, buyer); - - userRepository.save(seller); - auctionRepository.save(auction); - userRepository.save(buyer); - chatRoomRepository.save(chatRoom); - - final User invalidReporter = User.builder() - .name("사용자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12347") - .build(); - userRepository.save(invalidReporter); - - final CreateChatRoomReportDto createChatRoomReportDto = new CreateChatRoomReportDto( - chatRoom.getId(), - "신고합니다.", - invalidReporter.getId() - ); - // when & then - assertThatThrownBy(() -> chatRoomReportService.create(createChatRoomReportDto)) - .isInstanceOf(ChatRoomReportNotAccessibleException.class) + assertThatThrownBy(() -> chatRoomReportService.create(참여자가_아닌_사용자의_채팅방_신고_요청_dto)) + .isInstanceOf(InvalidChatRoomReportException.class) .hasMessage("해당 채팅방을 신고할 권한이 없습니다."); } @Test void 이미_신고한_채팅방을_동일_사용자가_신고하는_경우_예외가_발생한다() { - // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User buyer = User.builder() - .name("구매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - final ChatRoom chatRoom = new ChatRoom(auction, buyer); - - userRepository.save(seller); - auctionRepository.save(auction); - userRepository.save(buyer); - chatRoomRepository.save(chatRoom); - - final CreateChatRoomReportDto createChatRoomReportDto = new CreateChatRoomReportDto( - chatRoom.getId(), - "신고합니다.", - buyer.getId() - ); - - chatRoomReportService.create(createChatRoomReportDto); - // when & then - assertThatThrownBy(() -> chatRoomReportService.create(createChatRoomReportDto)) + assertThatThrownBy(() -> chatRoomReportService.create(이미_신고한_사용자의_채팅방_신고_요청_dto)) .isInstanceOf(AlreadyReportChatRoomException.class) .hasMessage("이미 신고한 채팅방입니다."); } @Test void 전체_신고_목록을_조회한다() { - // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction1 = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User user1 = User.builder() - .name("사용자1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - final Auction auction2 = Auction.builder() - .seller(seller) - .title("경매 상품 2") - .description("이것은 경매 상품 2 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final Auction auction3 = Auction.builder() - .seller(seller) - .title("경매 상품 3") - .description("이것은 경매 상품 3 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now().plusDays(7)) - .build(); - final User user2 = User.builder() - .name("사용자2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12347") - .build(); - final User user3 = User.builder() - .name("사용자3") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12348") - .build(); - final ChatRoom chatRoom1 = new ChatRoom(auction1, user1); - final ChatRoom chatRoom2 = new ChatRoom(auction2, user2); - final ChatRoom chatRoom3 = new ChatRoom(auction3, user3); - - userRepository.save(seller); - auctionRepository.save(auction1); - auctionRepository.save(auction2); - auctionRepository.save(auction3); - userRepository.save(user1); - userRepository.save(user2); - userRepository.save(user3); - chatRoomRepository.save(chatRoom1); - chatRoomRepository.save(chatRoom2); - chatRoomRepository.save(chatRoom3); - - final CreateChatRoomReportDto createChatRoomReportDto1 = new CreateChatRoomReportDto( - chatRoom1.getId(), - "신고합니다", - user1.getId() - ); - chatRoomReportService.create(createChatRoomReportDto1); - - final CreateChatRoomReportDto createChatRoomReportDto2 = new CreateChatRoomReportDto( - chatRoom2.getId(), - "신고합니다", - user2.getId() - ); - chatRoomReportService.create(createChatRoomReportDto2); - - final CreateChatRoomReportDto createChatRoomReportDto3 = new CreateChatRoomReportDto( - chatRoom3.getId(), - "신고합니다", - user3.getId() - ); - chatRoomReportService.create(createChatRoomReportDto3); - // when final List actual = chatRoomReportService.readAll(); // then SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual.get(0).reporterDto().id()).isEqualTo(user1.getId()); - softAssertions.assertThat(actual.get(0).chatRoomDto().id()).isEqualTo(chatRoom1.getId()); - softAssertions.assertThat(actual.get(1).reporterDto().id()).isEqualTo(user2.getId()); - softAssertions.assertThat(actual.get(1).chatRoomDto().id()).isEqualTo(chatRoom2.getId()); - softAssertions.assertThat(actual.get(2).reporterDto().id()).isEqualTo(user3.getId()); - softAssertions.assertThat(actual.get(2).chatRoomDto().id()).isEqualTo(chatRoom3.getId()); + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.get(0).reporterDto().id()).isEqualTo(이미_신고한_구매자1.getId()); + softAssertions.assertThat(actual.get(0).chatRoomDto().id()).isEqualTo(채팅방1.getId()); + softAssertions.assertThat(actual.get(1).reporterDto().id()).isEqualTo(이미_신고한_구매자2.getId()); + softAssertions.assertThat(actual.get(1).chatRoomDto().id()).isEqualTo(채팅방2.getId()); + softAssertions.assertThat(actual.get(2).reporterDto().id()).isEqualTo(이미_신고한_구매자3.getId()); + softAssertions.assertThat(actual.get(2).chatRoomDto().id()).isEqualTo(채팅방3.getId()); }); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/report/application/QuestionReportServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/report/application/QuestionReportServiceTest.java new file mode 100644 index 000000000..aea629942 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/report/application/QuestionReportServiceTest.java @@ -0,0 +1,85 @@ +package com.ddang.ddang.report.application; + +import com.ddang.ddang.configuration.IsolateDatabase; +import com.ddang.ddang.qna.application.exception.QuestionNotFoundException; +import com.ddang.ddang.report.application.dto.ReadQuestionReportDto; +import com.ddang.ddang.report.application.exception.InvalidQuestionReportException; +import com.ddang.ddang.report.application.fixture.QuestionReportServiceFixture; +import com.ddang.ddang.user.application.exception.UserNotFoundException; +import org.assertj.core.api.*; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@IsolateDatabase +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class QuestionReportServiceTest extends QuestionReportServiceFixture { + + @Autowired + QuestionReportService questionReportService; + + @Test + void 질문을_신고한다() { + // when + final Long actual = questionReportService.create(질문_신고_요청_dto); + + // then + assertThat(actual).isPositive(); + } + + @Test + void 존재하지_않는_질문_신고시_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> questionReportService.create(존재하지_않는_질문_신고_요청_dto)) + .isInstanceOf(QuestionNotFoundException.class) + .hasMessage("해당 질문을 찾을 수 없습니다."); + } + + @Test + void 존재하지_않는_사용자가_질문_신고시_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> questionReportService.create(존재하지_않는_사용자가_질문_신고_요청_dto)) + .isInstanceOf(UserNotFoundException.class) + .hasMessage("해당 사용자를 찾을 수 없습니다."); + } + + @Test + void 질문자가_본인_질문_신고시_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> questionReportService.create(질문자가_본인_질문_신고_요청_dto)) + .isInstanceOf(InvalidQuestionReportException.class) + .hasMessage("본인 질문입니다."); + } + + @Test + void 이미_신고한_질문_신고시_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> questionReportService.create(이미_신고한_질문_신고_요청_dto)) + .isInstanceOf(InvalidQuestionReportException.class) + .hasMessage("이미 신고한 질문입니다."); + } + + @Test + void 전체_신고_목록을_조회한다() { + // when + final List actual = questionReportService.readAll(); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.get(0).reporterDto().id()).isEqualTo(이미_신고한_신고자1.getId()); + softAssertions.assertThat(actual.get(0).id()).isEqualTo(질문_신고1.getId()); + softAssertions.assertThat(actual.get(1).reporterDto().id()).isEqualTo(이미_신고한_신고자2.getId()); + softAssertions.assertThat(actual.get(1).id()).isEqualTo(질문_신고2.getId()); + softAssertions.assertThat(actual.get(2).reporterDto().id()).isEqualTo(이미_신고한_신고자3.getId()); + softAssertions.assertThat(actual.get(2).id()).isEqualTo(질문_신고3.getId()); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/report/application/fixture/AnswerReportServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/report/application/fixture/AnswerReportServiceFixture.java new file mode 100644 index 000000000..82a5494b0 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/report/application/fixture/AnswerReportServiceFixture.java @@ -0,0 +1,141 @@ +package com.ddang.ddang.report.application.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.qna.domain.Answer; +import com.ddang.ddang.qna.domain.Question; +import com.ddang.ddang.qna.infrastructure.JpaAnswerRepository; +import com.ddang.ddang.qna.infrastructure.JpaQuestionRepository; +import com.ddang.ddang.report.application.dto.CreateAnswerReportDto; +import com.ddang.ddang.report.domain.AnswerReport; +import com.ddang.ddang.report.infrastructure.persistence.JpaAnswerReportRepository; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class AnswerReportServiceFixture { + + @Autowired + private JpaCategoryRepository categoryRepository; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaQuestionRepository questionRepository; + + @Autowired + private JpaAnswerRepository answerRepository; + + @Autowired + private JpaAnswerReportRepository answerReportRepository; + + protected User 신고자; + protected User 이미_신고한_신고자1; + protected User 이미_신고한_신고자2; + protected User 이미_신고한_신고자3; + protected Answer 답변; + protected AnswerReport 답변_신고1; + protected AnswerReport 답변_신고2; + protected AnswerReport 답변_신고3; + protected CreateAnswerReportDto 답변_신고_요청_dto; + protected CreateAnswerReportDto 존재하지_않는_답변_신고_요청_dto; + protected CreateAnswerReportDto 존재하지_않는_사용자가_답변_신고_요청_dto; + protected CreateAnswerReportDto 답변자가_본인_답변_신고_요청_dto; + protected CreateAnswerReportDto 이미_신고한_답변_신고_요청_dto; + + @BeforeEach + void setUp() { + final Long 존재하지_않는_답변_아이디 = -999L; + final Long 존재하지_않는_사용자_아이디 = -999L; + + final ProfileImage 프로필_이미지 = new ProfileImage("프로필.jpg", "프로필.jpg"); + final User 판매자 = User.builder() + .name("판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + final User 질문자 = User.builder() + .name("질문자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + final User 답변자 = 판매자; + 신고자 = User.builder() + .name("신고자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + 이미_신고한_신고자1 = User.builder() + .name("이미 신고한 신고자1") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12348") + .build(); + 이미_신고한_신고자2 = User.builder() + .name("이미 신고한 신고자2") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12349") + .build(); + 이미_신고한_신고자3 = User.builder() + .name("이미 신고한 신고자3") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12350") + .build(); + + final Category 전자기기_카테고리 = new Category("전자기기"); + final Category 전자기기_서브_노트북_카테고리 = new Category("노트북 카테고리"); + 전자기기_카테고리.addSubCategory(전자기기_서브_노트북_카테고리); + final AuctionImage 경매_이미지 = new AuctionImage("경매이미지.jpg", "경매이미지.jpg"); + final Auction 경매 = Auction.builder() + .seller(판매자) + .title("경매 상품") + .description("이것은 경매 상품입니다.") + .subCategory(전자기기_서브_노트북_카테고리) + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 경매.addAuctionImages(List.of(경매_이미지)); + + final Question 질문 = new Question(경매, 질문자, "질문드립니다."); + 답변 = new Answer("답변드립니다."); + 질문.addAnswer(답변); + 답변_신고1 = new AnswerReport(이미_신고한_신고자1, 답변, "신고합니다."); + 답변_신고2 = new AnswerReport(이미_신고한_신고자2, 답변, "신고합니다."); + 답변_신고3 = new AnswerReport(이미_신고한_신고자3, 답변, "신고합니다."); + + userRepository.saveAll(List.of(판매자, 질문자, 답변자, 신고자, 이미_신고한_신고자1, 이미_신고한_신고자2, 이미_신고한_신고자3)); + categoryRepository.saveAll(List.of(전자기기_카테고리, 전자기기_서브_노트북_카테고리)); + auctionRepository.save(경매); + questionRepository.save(질문); + answerRepository.save(답변); + answerReportRepository.saveAll(List.of(답변_신고1, 답변_신고2, 답변_신고3)); + + 답변_신고_요청_dto = new CreateAnswerReportDto(답변.getId(), "신고합니다.", 신고자.getId()); + 존재하지_않는_답변_신고_요청_dto = new CreateAnswerReportDto(존재하지_않는_답변_아이디, "신고합니다.", 신고자.getId()); + 존재하지_않는_사용자가_답변_신고_요청_dto = new CreateAnswerReportDto(답변.getId(), "신고합니다.", 존재하지_않는_사용자_아이디); + 답변자가_본인_답변_신고_요청_dto = new CreateAnswerReportDto(답변.getId(), "신고합니다.", 답변자.getId()); + 이미_신고한_답변_신고_요청_dto = new CreateAnswerReportDto(답변.getId(), "신고합니다.", 이미_신고한_신고자1.getId()); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/report/application/fixture/AuctionReportServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/report/application/fixture/AuctionReportServiceFixture.java new file mode 100644 index 000000000..eac55d091 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/report/application/fixture/AuctionReportServiceFixture.java @@ -0,0 +1,166 @@ +package com.ddang.ddang.report.application.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.image.infrastructure.persistence.JpaAuctionImageRepository; +import com.ddang.ddang.image.infrastructure.persistence.JpaProfileImageRepository; +import com.ddang.ddang.report.application.dto.CreateAuctionReportDto; +import com.ddang.ddang.report.domain.AuctionReport; +import com.ddang.ddang.report.infrastructure.persistence.JpaAuctionReportRepository; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class AuctionReportServiceFixture { + + @Autowired + private JpaCategoryRepository categoryRepository; + + @Autowired + private JpaProfileImageRepository profileImageRepository; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaAuctionImageRepository auctionImageRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaAuctionReportRepository auctionReportRepository; + + protected User 이미_신고한_신고자1; + protected User 이미_신고한_신고자2; + protected User 이미_신고한_신고자3; + protected Auction 경매; + + protected CreateAuctionReportDto 새로운_경매_신고_요청_dto; + protected CreateAuctionReportDto 존재하지_않는_사용자의_경매_신고_요청_dto; + protected CreateAuctionReportDto 존재하지_않는_경매_신고_요청_dto; + protected CreateAuctionReportDto 판매자가_본인의_경매_신고_요청_dto; + protected CreateAuctionReportDto 삭제된_경매_신고_요청_dto; + protected CreateAuctionReportDto 이미_신고한_사용자가_경매_신고_요청_dto; + + @BeforeEach + void setUp() { + final Long 존재하지_않는_사용자_아이디 = -9999L; + final Long 존재하지_않는_경매_아이디 = -9999L; + + final ProfileImage 프로필_이미지 = new ProfileImage("프로필.jpg", "프로필.jpg"); + final User 판매자 = User.builder() + .name("판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + final User 새로운_신고자 = User.builder() + .name("새로운_신고자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + 이미_신고한_신고자1 = User.builder() + .name("신고자1") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + 이미_신고한_신고자2 = User.builder() + .name("신고자2") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12348") + .build(); + 이미_신고한_신고자3 = User.builder() + .name("신고자3") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12349") + .build(); + + final Category 전자기기_카테고리 = new Category("전자기기"); + final Category 전자기기_서브_노트북_카테고리 = new Category("노트북 카테고리"); + 전자기기_카테고리.addSubCategory(전자기기_서브_노트북_카테고리); + final AuctionImage 경매_이미지 = new AuctionImage("경매이미지.jpg", "경매이미지.jpg"); + 경매 = Auction.builder() + .seller(판매자) + .title("경매 상품") + .description("이것은 경매 상품입니다.") + .subCategory(전자기기_서브_노트북_카테고리) + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 경매.addAuctionImages(List.of(경매_이미지)); + + final Auction 삭제된_경매 = Auction.builder() + .seller(판매자) + .title("삭제된 경매 상품") + .description("이것은 삭제된 경매 상품입니다.") + .subCategory(전자기기_서브_노트북_카테고리) + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 삭제된_경매.addAuctionImages(List.of(경매_이미지)); + 삭제된_경매.delete(); + + final AuctionReport 경매_신고1 = new AuctionReport(이미_신고한_신고자1, 경매, "신고합니다"); + final AuctionReport 경매_신고2 = new AuctionReport(이미_신고한_신고자2, 경매, "신고합니다"); + final AuctionReport 경매_신고3 = new AuctionReport(이미_신고한_신고자3, 경매, "신고합니다"); + + profileImageRepository.save(프로필_이미지); + userRepository.saveAll(List.of(판매자, 새로운_신고자, 이미_신고한_신고자1, 이미_신고한_신고자2, 이미_신고한_신고자3)); + + categoryRepository.saveAll(List.of(전자기기_카테고리, 전자기기_서브_노트북_카테고리)); + auctionImageRepository.save(경매_이미지); + auctionRepository.saveAll(List.of(경매, 삭제된_경매)); + + auctionReportRepository.saveAll(List.of(경매_신고1, 경매_신고2, 경매_신고3)); + + 새로운_경매_신고_요청_dto = new CreateAuctionReportDto( + 경매.getId(), + "신고합니다", + 새로운_신고자.getId() + ); + 존재하지_않는_사용자의_경매_신고_요청_dto = new CreateAuctionReportDto( + 경매.getId(), + "신고합니다", + 존재하지_않는_사용자_아이디 + ); + 존재하지_않는_경매_신고_요청_dto = new CreateAuctionReportDto( + 존재하지_않는_경매_아이디, + "신고합니다", + 새로운_신고자.getId() + ); + 판매자가_본인의_경매_신고_요청_dto = new CreateAuctionReportDto( + 경매.getId(), + "신고합니다", + 판매자.getId() + ); + 삭제된_경매_신고_요청_dto = new CreateAuctionReportDto( + 삭제된_경매.getId(), + "신고합니다", + 새로운_신고자.getId() + ); + 이미_신고한_사용자가_경매_신고_요청_dto = new CreateAuctionReportDto( + 경매.getId(), + "신고합니다", + 이미_신고한_신고자1.getId() + ); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/report/application/fixture/ChatRoomReportServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/report/application/fixture/ChatRoomReportServiceFixture.java new file mode 100644 index 000000000..0f2ac6fda --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/report/application/fixture/ChatRoomReportServiceFixture.java @@ -0,0 +1,182 @@ +package com.ddang.ddang.report.application.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.image.infrastructure.persistence.JpaAuctionImageRepository; +import com.ddang.ddang.image.infrastructure.persistence.JpaProfileImageRepository; +import com.ddang.ddang.report.application.dto.CreateChatRoomReportDto; +import com.ddang.ddang.report.domain.ChatRoomReport; +import com.ddang.ddang.report.infrastructure.persistence.JpaChatRoomReportRepository; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class ChatRoomReportServiceFixture { + + @Autowired + private JpaCategoryRepository categoryRepository; + + @Autowired + private JpaProfileImageRepository profileImageRepository; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaAuctionImageRepository auctionImageRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaChatRoomReportRepository chatRoomReportRepository; + + @Autowired + private JpaChatRoomRepository chatRoomRepository; + + protected User 이미_신고한_구매자1; + protected User 이미_신고한_구매자2; + protected User 이미_신고한_구매자3; + protected ChatRoom 채팅방1; + protected ChatRoom 채팅방2; + protected ChatRoom 채팅방3; + + protected CreateChatRoomReportDto 채팅방_신고_요청_dto; + protected CreateChatRoomReportDto 존재하지_않는_사용자의_채팅방_신고_요청_dto; + protected CreateChatRoomReportDto 존재하지_않는_채팅방_신고_요청_dto; + protected CreateChatRoomReportDto 참여자가_아닌_사용자의_채팅방_신고_요청_dto; + protected CreateChatRoomReportDto 이미_신고한_사용자의_채팅방_신고_요청_dto; + + @BeforeEach + void setUp() { + final Long 존재하지_않는_사용자_아이디 = -9999L; + final Long 존재하지_않는_채팅방_아이디 = -9999L; + + final ProfileImage 프로필_이미지 = new ProfileImage("프로필.jpg", "프로필.jpg"); + final User 판매자 = User.builder() + .name("판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + final User 판매자겸_아직_신고하지_않은_신고자 = 판매자; + 이미_신고한_구매자1 = User.builder() + .name("구매자1") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + 이미_신고한_구매자2 = User.builder() + .name("구매자2") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + 이미_신고한_구매자3 = User.builder() + .name("구매자3") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12348") + .build(); + final User 채팅방_참여자가_아닌_사용자 = User.builder() + .name("채팅방_참여자가_아닌_사용자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12349") + .build(); + + final Category 전자기기_카테고리 = new Category("전자기기"); + final Category 전자기기_서브_노트북_카테고리 = new Category("노트북 카테고리"); + 전자기기_카테고리.addSubCategory(전자기기_서브_노트북_카테고리); + final AuctionImage 경매_이미지 = new AuctionImage("경매이미지.jpg", "경매이미지.jpg"); + final Auction 경매1 = Auction.builder() + .seller(판매자겸_아직_신고하지_않은_신고자) + .title("경매 상품") + .description("이것은 경매 상품입니다.") + .subCategory(전자기기_서브_노트북_카테고리) + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 경매1.addAuctionImages(List.of(경매_이미지)); + final Auction 경매2 = Auction.builder() + .seller(판매자겸_아직_신고하지_않은_신고자) + .title("경매 상품") + .description("이것은 경매 상품입니다.") + .subCategory(전자기기_서브_노트북_카테고리) + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 경매2.addAuctionImages(List.of(경매_이미지)); + final Auction 경매3 = Auction.builder() + .seller(판매자겸_아직_신고하지_않은_신고자) + .title("경매 상품") + .description("이것은 경매 상품입니다.") + .subCategory(전자기기_서브_노트북_카테고리) + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 경매3.addAuctionImages(List.of(경매_이미지)); + + 채팅방1 = new ChatRoom(경매1, 이미_신고한_구매자1); + 채팅방2 = new ChatRoom(경매2, 이미_신고한_구매자2); + 채팅방3 = new ChatRoom(경매3, 이미_신고한_구매자3); + + final ChatRoomReport 채팅방_신고1 = new ChatRoomReport(이미_신고한_구매자1, 채팅방1, "신고합니다."); + final ChatRoomReport 채팅방_신고2 = new ChatRoomReport(이미_신고한_구매자2, 채팅방2, "신고합니다."); + final ChatRoomReport 채팅방_신고3 = new ChatRoomReport(이미_신고한_구매자3, 채팅방3, "신고합니다."); + + profileImageRepository.save(프로필_이미지); + userRepository.saveAll(List.of(판매자겸_아직_신고하지_않은_신고자, 이미_신고한_구매자1, 이미_신고한_구매자2, 이미_신고한_구매자3, 채팅방_참여자가_아닌_사용자)); + + categoryRepository.saveAll(List.of(전자기기_카테고리, 전자기기_서브_노트북_카테고리)); + auctionImageRepository.save(경매_이미지); + auctionRepository.saveAll(List.of(경매1, 경매2, 경매3)); + + chatRoomRepository.saveAll(List.of(채팅방1, 채팅방2, 채팅방3)); + + chatRoomReportRepository.saveAll(List.of(채팅방_신고1, 채팅방_신고2, 채팅방_신고3)); + + 채팅방_신고_요청_dto = new CreateChatRoomReportDto( + 채팅방1.getId(), + "신고합니다.", + 판매자겸_아직_신고하지_않은_신고자.getId() + ); + 존재하지_않는_사용자의_채팅방_신고_요청_dto = new CreateChatRoomReportDto( + 채팅방1.getId(), + "신고합니다.", + 존재하지_않는_사용자_아이디 + ); + 존재하지_않는_채팅방_신고_요청_dto = new CreateChatRoomReportDto( + 존재하지_않는_채팅방_아이디, + "신고합니다.", + 판매자겸_아직_신고하지_않은_신고자.getId() + ); + 참여자가_아닌_사용자의_채팅방_신고_요청_dto = new CreateChatRoomReportDto( + 채팅방1.getId(), + "신고합니다.", + 채팅방_참여자가_아닌_사용자.getId() + ); + 이미_신고한_사용자의_채팅방_신고_요청_dto = new CreateChatRoomReportDto( + 채팅방1.getId(), + "신고합니다.", + 이미_신고한_구매자1.getId() + ); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/report/application/fixture/QuestionReportServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/report/application/fixture/QuestionReportServiceFixture.java new file mode 100644 index 000000000..88832351d --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/report/application/fixture/QuestionReportServiceFixture.java @@ -0,0 +1,131 @@ +package com.ddang.ddang.report.application.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.qna.domain.Question; +import com.ddang.ddang.qna.infrastructure.JpaQuestionRepository; +import com.ddang.ddang.report.application.dto.CreateQuestionReportDto; +import com.ddang.ddang.report.domain.QuestionReport; +import com.ddang.ddang.report.infrastructure.persistence.JpaQuestionReportRepository; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class QuestionReportServiceFixture { + + @Autowired + private JpaCategoryRepository categoryRepository; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaQuestionRepository questionRepository; + + @Autowired + private JpaQuestionReportRepository questionReportRepository; + + protected User 이미_신고한_신고자1; + protected User 이미_신고한_신고자2; + protected User 이미_신고한_신고자3; + protected QuestionReport 질문_신고1; + protected QuestionReport 질문_신고2; + protected QuestionReport 질문_신고3; + + protected CreateQuestionReportDto 질문_신고_요청_dto; + protected CreateQuestionReportDto 존재하지_않는_사용자가_질문_신고_요청_dto; + protected CreateQuestionReportDto 존재하지_않는_질문_신고_요청_dto; + protected CreateQuestionReportDto 질문자가_본인_질문_신고_요청_dto; + protected CreateQuestionReportDto 이미_신고한_질문_신고_요청_dto; + + @BeforeEach + void setUp() { + final Long 존재하지_않는_질문_아이디 = -999L; + final Long 존재하지_않는_사용자_아이디 = -999L; + + final ProfileImage 프로필_이미지 = new ProfileImage("프로필.jpg", "프로필.jpg"); + final User 판매자 = User.builder() + .name("판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + final User 질문자 = User.builder() + .name("질문자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + final User 신고자 = User.builder() + .name("신고자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + 이미_신고한_신고자1 = User.builder() + .name("이미 신고한 신고자1") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12348") + .build(); + 이미_신고한_신고자2 = User.builder() + .name("이미 신고한 신고자2") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12349") + .build(); + 이미_신고한_신고자3 = User.builder() + .name("이미 신고한 신고자3") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12350") + .build(); + + final Category 전자기기_카테고리 = new Category("전자기기"); + final Category 전자기기_서브_노트북_카테고리 = new Category("노트북 카테고리"); + 전자기기_카테고리.addSubCategory(전자기기_서브_노트북_카테고리); + final AuctionImage 경매_이미지 = new AuctionImage("경매이미지.jpg", "경매이미지.jpg"); + final Auction 경매 = Auction.builder() + .seller(판매자) + .title("경매 상품") + .description("이것은 경매 상품입니다.") + .subCategory(전자기기_서브_노트북_카테고리) + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 경매.addAuctionImages(List.of(경매_이미지)); + + final Question 질문 = new Question(경매, 질문자, "질문드립니다."); + 질문_신고1 = new QuestionReport(이미_신고한_신고자1, 질문, "신고합니다."); + 질문_신고2 = new QuestionReport(이미_신고한_신고자2, 질문, "신고합니다."); + 질문_신고3 = new QuestionReport(이미_신고한_신고자3, 질문, "신고합니다."); + + userRepository.saveAll(List.of(판매자, 질문자, 신고자, 이미_신고한_신고자1, 이미_신고한_신고자2, 이미_신고한_신고자3)); + categoryRepository.saveAll(List.of(전자기기_카테고리, 전자기기_서브_노트북_카테고리)); + auctionRepository.save(경매); + questionRepository.save(질문); + questionReportRepository.saveAll(List.of(질문_신고1, 질문_신고2, 질문_신고3)); + + 질문_신고_요청_dto = new CreateQuestionReportDto(질문.getId(), "신고합니다.", 신고자.getId()); + 존재하지_않는_질문_신고_요청_dto = new CreateQuestionReportDto(존재하지_않는_질문_아이디, "신고합니다.", 신고자.getId()); + 존재하지_않는_사용자가_질문_신고_요청_dto = new CreateQuestionReportDto(질문.getId(), "신고합니다.", 존재하지_않는_사용자_아이디); + 질문자가_본인_질문_신고_요청_dto = new CreateQuestionReportDto(질문.getId(), "신고합니다.", 질문자.getId()); + 이미_신고한_질문_신고_요청_dto = new CreateQuestionReportDto(질문.getId(), "신고합니다.", 이미_신고한_신고자1.getId()); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/JpaAnswerReportRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/JpaAnswerReportRepositoryTest.java new file mode 100644 index 000000000..4e1a9c379 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/JpaAnswerReportRepositoryTest.java @@ -0,0 +1,72 @@ +package com.ddang.ddang.report.infrastructure.persistence; + +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.ddang.ddang.report.domain.AnswerReport; +import com.ddang.ddang.report.infrastructure.persistence.fixture.JpaAnswerReportRepositoryFixture; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class JpaAnswerReportRepositoryTest extends JpaAnswerReportRepositoryFixture { + + @Autowired + JpaAnswerReportRepository answerReportRepository; + + @Test + void 답변을_등록한다() { + // given + final AnswerReport answerReport = new AnswerReport(신고자, 답변, 신고_내용); + + // when + final AnswerReport actual = answerReportRepository.save(answerReport); + + // then + assertThat(actual.getId()).isPositive(); + } + + @Test + void 신고한_답변이라면_참을_반환한다() { + // when + final boolean actual = answerReportRepository.existsByAnswerIdAndReporterId(이미_신고된_답변.getId(), 신고자.getId()); + + // then + assertThat(actual).isTrue(); + } + + @Test + void 신고_전인_답변이라면_거짓을_반환한다() { + // when + final boolean actual = answerReportRepository.existsByAnswerIdAndReporterId(답변.getId(), 신고자.getId()); + + // then + assertThat(actual).isFalse(); + } + + @Test + void 신고된_답변_목록을_조회한다() { + // when + final List actual = answerReportRepository.findAllByOrderByIdAsc(); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(4); + softAssertions.assertThat(actual.get(0)).isEqualTo(답변_신고1); + softAssertions.assertThat(actual.get(1)).isEqualTo(답변_신고2); + softAssertions.assertThat(actual.get(2)).isEqualTo(답변_신고3); + softAssertions.assertThat(actual.get(3)).isEqualTo(답변_신고4); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/JpaAuctionReportRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/JpaAuctionReportRepositoryTest.java index faab48c58..20dd7cfa4 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/JpaAuctionReportRepositoryTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/JpaAuctionReportRepositoryTest.java @@ -1,15 +1,11 @@ package com.ddang.ddang.report.infrastructure.persistence; -import com.ddang.ddang.auction.domain.Auction; -import com.ddang.ddang.auction.domain.BidUnit; -import com.ddang.ddang.auction.domain.Price; -import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; import com.ddang.ddang.configuration.JpaConfiguration; import com.ddang.ddang.configuration.QuerydslConfiguration; import com.ddang.ddang.report.domain.AuctionReport; -import com.ddang.ddang.user.domain.User; -import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import com.ddang.ddang.report.infrastructure.persistence.fixture.JpaAuctionReportRepositoryFixture; import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; @@ -18,119 +14,41 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -import java.time.LocalDateTime; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -@Import({JpaConfiguration.class, QuerydslConfiguration.class}) -class JpaAuctionReportRepositoryTest { +class JpaAuctionReportRepositoryTest extends JpaAuctionReportRepositoryFixture { - @Autowired + @PersistenceContext EntityManager em; @Autowired JpaAuctionReportRepository auctionReportRepository; - @Autowired - JpaAuctionRepository auctionRepository; - - @Autowired - JpaUserRepository userRepository; - @Test void 경매_신고를_저장한다() { // given - final Auction auction = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final User user = User.builder() - .name("사용자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final AuctionReport auctionReport = new AuctionReport(user, auction, "신고합니다"); - - auctionRepository.save(auction); - userRepository.save(user); + final AuctionReport auctionReport = new AuctionReport(판매자, 경매, "신고합니다"); // when - auctionReportRepository.save(auctionReport); + final AuctionReport actual = auctionReportRepository.save(auctionReport); // then em.flush(); em.clear(); - assertThat(auctionReport.getId()).isPositive(); + assertThat(actual.getId()).isPositive(); } @Test void 특정_경매_아이디와_신고자_아이디가_동일한_레코드가_존재하면_참을_반환한다() { - // given - final Auction auction = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final User user = User.builder() - .name("사용자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final AuctionReport auctionReport = new AuctionReport(user, auction, "신고합니다"); - - auctionRepository.save(auction); - userRepository.save(user); - auctionReportRepository.save(auctionReport); - - em.flush(); - em.clear(); - // when - final boolean actual = auctionReportRepository.existsByAuctionIdAndReporterId(auction.getId(), user.getId()); - - // then - assertThat(actual).isTrue(); - } - - @Test - void 특정_경매_아이디와_신고자_아이디가_동일한_레코드가_존재한다면_참을_반환한다() { - // given - final Auction auction = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final User user = User.builder() - .name("사용자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final AuctionReport auctionReport = new AuctionReport(user, auction, "신고합니다"); - - auctionRepository.save(auction); - userRepository.save(user); - auctionReportRepository.save(auctionReport); - - em.flush(); - em.clear(); - - // when - final boolean actual = auctionReportRepository.existsByAuctionIdAndReporterId(auction.getId(), user.getId()); + final boolean actual = auctionReportRepository.existsByAuctionIdAndReporterId(경매.getId(), 신고자1.getId()); // then assertThat(actual).isTrue(); @@ -138,12 +56,11 @@ class JpaAuctionReportRepositoryTest { @Test void 특정_경매_아이디와_신고자_아이디가_동일한_레코드가_존재하지_않는다면_거짓을_반환한다() { - // given - final long invalidAuctionId = -9999L; - final long invalidUserId = -9999L; - // when - final boolean actual = auctionReportRepository.existsByAuctionIdAndReporterId(invalidAuctionId, invalidUserId); + final boolean actual = auctionReportRepository.existsByAuctionIdAndReporterId( + 존재하지_않는_경매_아이디, + 존재하지_않는_사용자_아이디 + ); // then assertThat(actual).isFalse(); @@ -151,70 +68,15 @@ class JpaAuctionReportRepositoryTest { @Test void 전체_경매_신고_목록을_조회한다() { - // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final User user1 = User.builder() - .name("사용자1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - final User user2 = User.builder() - .name("사용자2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12347") - .build(); - final User user3 = User.builder() - .name("사용자3") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12348") - .build(); - final AuctionReport auctionReport1 = new AuctionReport(user1, auction, "신고합니다"); - final AuctionReport auctionReport2 = new AuctionReport(user2, auction, "신고합니다"); - final AuctionReport auctionReport3 = new AuctionReport(user3, auction, "신고합니다"); - - userRepository.save(seller); - auctionRepository.save(auction); - userRepository.save(user1); - userRepository.save(user2); - userRepository.save(user3); - - auctionReportRepository.save(auctionReport1); - auctionReportRepository.save(auctionReport2); - auctionReportRepository.save(auctionReport3); - - em.flush(); - em.clear(); - // when - final List actual = auctionReportRepository.findAll(); + final List actual = auctionReportRepository.findAllByOrderByIdAsc(); // then SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual.get(0).getReporter()).isEqualTo(user1); - softAssertions.assertThat(actual.get(0).getAuction()).isEqualTo(auction); - softAssertions.assertThat(actual.get(0).getAuction().getSeller()).isEqualTo(seller); - softAssertions.assertThat(actual.get(1).getReporter()).isEqualTo(user2); - softAssertions.assertThat(actual.get(1).getAuction()).isEqualTo(auction); - softAssertions.assertThat(actual.get(1).getAuction().getSeller()).isEqualTo(seller); - softAssertions.assertThat(actual.get(2).getReporter()).isEqualTo(user3); - softAssertions.assertThat(actual.get(2).getAuction()).isEqualTo(auction); - softAssertions.assertThat(actual.get(2).getAuction().getSeller()).isEqualTo(seller); + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.get(0)).isEqualTo(경매_신고1); + softAssertions.assertThat(actual.get(1)).isEqualTo(경매_신고2); + softAssertions.assertThat(actual.get(2)).isEqualTo(경매_신고3); }); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/JpaChatRoomReportRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/JpaChatRoomReportRepositoryTest.java index 290423dfe..4bd9d2bf1 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/JpaChatRoomReportRepositoryTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/JpaChatRoomReportRepositoryTest.java @@ -1,17 +1,11 @@ package com.ddang.ddang.report.infrastructure.persistence; -import com.ddang.ddang.auction.domain.Auction; -import com.ddang.ddang.auction.domain.BidUnit; -import com.ddang.ddang.auction.domain.Price; -import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; -import com.ddang.ddang.chat.domain.ChatRoom; -import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; import com.ddang.ddang.configuration.JpaConfiguration; import com.ddang.ddang.configuration.QuerydslConfiguration; import com.ddang.ddang.report.domain.ChatRoomReport; -import com.ddang.ddang.user.domain.User; -import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import com.ddang.ddang.report.infrastructure.persistence.fixture.JpaChatRoomReportRepositoryFixture; import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; @@ -20,94 +14,40 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -import java.time.LocalDateTime; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -@Import({JpaConfiguration.class, QuerydslConfiguration.class}) -class JpaChatRoomReportRepositoryTest { +class JpaChatRoomReportRepositoryTest extends JpaChatRoomReportRepositoryFixture { - @Autowired + @PersistenceContext EntityManager em; @Autowired JpaChatRoomReportRepository chatRoomReportRepository; - @Autowired - JpaChatRoomRepository chatRoomRepository; - - @Autowired - JpaAuctionRepository auctionRepository; - - @Autowired - JpaUserRepository userRepository; - @Test void 채팅방_신고를_저장한다() { - // given - final Auction auction = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final User buyer = User.builder() - .name("사용자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final ChatRoom chatRoom = new ChatRoom(auction, buyer); - final ChatRoomReport chatRoomReport = new ChatRoomReport(buyer, chatRoom, "신고합니다."); - - auctionRepository.save(auction); - userRepository.save(buyer); - chatRoomRepository.save(chatRoom); + final ChatRoomReport chatRoomReport = new ChatRoomReport(구매자1, 채팅방1, "신고합니다."); // when - chatRoomReportRepository.save(chatRoomReport); + final ChatRoomReport actual = chatRoomReportRepository.save(chatRoomReport); // then em.flush(); em.clear(); - assertThat(chatRoomReport.getId()).isPositive(); + assertThat(actual.getId()).isPositive(); } @Test void 특정_채팅방_아이디와_신고자_아이디가_동일한_레코드가_존재한다면_참을_반환한다() { - // given - final Auction auction = Auction.builder() - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final User buyer = User.builder() - .name("사용자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final ChatRoom chatRoom = new ChatRoom(auction, buyer); - final ChatRoomReport chatRoomReport = new ChatRoomReport(buyer, chatRoom, "신고합니다."); - - auctionRepository.save(auction); - userRepository.save(buyer); - chatRoomRepository.save(chatRoom); - chatRoomReportRepository.save(chatRoomReport); - - em.flush(); - em.clear(); - // when - final boolean actual = chatRoomReportRepository.existsByChatRoomIdAndReporterId(chatRoom.getId(), buyer.getId()); + final boolean actual = chatRoomReportRepository.existsByChatRoomIdAndReporterId(채팅방1.getId(), 구매자1겸_신고자.getId()); // then assertThat(actual).isTrue(); @@ -115,12 +55,11 @@ class JpaChatRoomReportRepositoryTest { @Test void 특정_채팅방_아이디와_신고자_아이디가_동일한_레코드가_존재하지_않는다면_거짓을_반환한다() { - // given - final long invalidChatRoom = -9999L; - final long invalidUserId = -9999L; - // when - final boolean actual = chatRoomReportRepository.existsByChatRoomIdAndReporterId(invalidChatRoom, invalidUserId); + final boolean actual = chatRoomReportRepository.existsByChatRoomIdAndReporterId( + 존재하지_않는_채팅방_아이디, + 존재하지_않는_사용자_아이디 + ); // then assertThat(actual).isFalse(); @@ -128,94 +67,15 @@ class JpaChatRoomReportRepositoryTest { @Test void 전체_채팅방_신고_목록을_조회한다() { - // given - final User seller = User.builder() - .name("판매자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - final Auction auction1 = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final Auction auction2 = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final Auction auction3 = Auction.builder() - .seller(seller) - .title("경매 상품 1") - .description("이것은 경매 상품 1 입니다.") - .bidUnit(new BidUnit(1_000)) - .startPrice(new Price(1_000)) - .closingTime(LocalDateTime.now()) - .build(); - final User user1 = User.builder() - .name("사용자1") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12346") - .build(); - final User user2 = User.builder() - .name("사용자2") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12347") - .build(); - final User user3 = User.builder() - .name("사용자3") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12348") - .build(); - final ChatRoom chatRoom1 = new ChatRoom(auction1, user1); - final ChatRoom chatRoom2 = new ChatRoom(auction2, user2); - final ChatRoom chatRoom3 = new ChatRoom(auction3, user3); - final ChatRoomReport chatRoomReport1 = new ChatRoomReport(user1, chatRoom1, "신고합니다"); - final ChatRoomReport chatRoomReport2 = new ChatRoomReport(user2, chatRoom2, "신고합니다"); - final ChatRoomReport chatRoomReport3 = new ChatRoomReport(user3, chatRoom3, "신고합니다"); - - userRepository.save(seller); - auctionRepository.save(auction1); - auctionRepository.save(auction2); - auctionRepository.save(auction3); - userRepository.save(user1); - userRepository.save(user2); - userRepository.save(user3); - chatRoomRepository.save(chatRoom1); - chatRoomRepository.save(chatRoom2); - chatRoomRepository.save(chatRoom3); - - chatRoomReportRepository.save(chatRoomReport1); - chatRoomReportRepository.save(chatRoomReport2); - chatRoomReportRepository.save(chatRoomReport3); - - em.flush(); - em.clear(); - // when - final List actual = chatRoomReportRepository.findAll(); + final List actual = chatRoomReportRepository.findAllByOrderByIdAsc(); // then SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual.get(0).getReporter()).isEqualTo(user1); - softAssertions.assertThat(actual.get(0).getChatRoom()).isEqualTo(chatRoom1); - softAssertions.assertThat(actual.get(0).getChatRoom().getAuction().getSeller()).isEqualTo(seller); - softAssertions.assertThat(actual.get(1).getReporter()).isEqualTo(user2); - softAssertions.assertThat(actual.get(1).getChatRoom()).isEqualTo(chatRoom2); - softAssertions.assertThat(actual.get(1).getChatRoom().getAuction().getSeller()).isEqualTo(seller); - softAssertions.assertThat(actual.get(2).getReporter()).isEqualTo(user3); - softAssertions.assertThat(actual.get(2).getChatRoom()).isEqualTo(chatRoom3); - softAssertions.assertThat(actual.get(2).getChatRoom().getAuction().getSeller()).isEqualTo(seller); + softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual.get(0)).isEqualTo(채팅방_신고1); + softAssertions.assertThat(actual.get(1)).isEqualTo(채팅방_신고2); + softAssertions.assertThat(actual.get(2)).isEqualTo(채팅방_신고3); }); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/JpaQuestionReportRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/JpaQuestionReportRepositoryTest.java new file mode 100644 index 000000000..7c85405e7 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/JpaQuestionReportRepositoryTest.java @@ -0,0 +1,72 @@ +package com.ddang.ddang.report.infrastructure.persistence; + +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.ddang.ddang.report.domain.QuestionReport; +import com.ddang.ddang.report.infrastructure.persistence.fixture.JpaQuestionReportRepositoryFixture; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class JpaQuestionReportRepositoryTest extends JpaQuestionReportRepositoryFixture { + + @Autowired + JpaQuestionReportRepository questionReportRepository; + + @Test + void 질문_신고를_등록한다() { + // given + final QuestionReport questionReport = new QuestionReport(신고자, 질문, 신고_내용); + + // when + final QuestionReport actual = questionReportRepository.save(questionReport); + + // then + assertThat(actual.getId()).isPositive(); + } + + @Test + void 신고한_질문이라면_참을_반환한다() { + // when + final boolean actual = questionReportRepository.existsByQuestionIdAndReporterId(이미_신고한_질문.getId(), 신고자.getId()); + + // then + assertThat(actual).isTrue(); + } + + @Test + void 신고_전인_질문이라면_거짓을_반환한다() { + // when + final boolean actual = questionReportRepository.existsByQuestionIdAndReporterId(질문.getId(), 신고자.getId()); + + // then + assertThat(actual).isFalse(); + } + + @Test + void 신고된_질문_목록을_조회한다() { + // when + final List actual = questionReportRepository.findAllByOrderByIdAsc(); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(4); + softAssertions.assertThat(actual.get(0)).isEqualTo(질문_신고1); + softAssertions.assertThat(actual.get(1)).isEqualTo(질문_신고2); + softAssertions.assertThat(actual.get(2)).isEqualTo(질문_신고3); + softAssertions.assertThat(actual.get(3)).isEqualTo(질문_신고4); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/fixture/JpaAnswerReportRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/fixture/JpaAnswerReportRepositoryFixture.java new file mode 100644 index 000000000..dd47fd348 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/fixture/JpaAnswerReportRepositoryFixture.java @@ -0,0 +1,140 @@ +package com.ddang.ddang.report.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.qna.domain.Answer; +import com.ddang.ddang.qna.domain.Question; +import com.ddang.ddang.qna.infrastructure.JpaAnswerRepository; +import com.ddang.ddang.qna.infrastructure.JpaQuestionRepository; +import com.ddang.ddang.report.domain.AnswerReport; +import com.ddang.ddang.report.infrastructure.persistence.JpaAnswerReportRepository; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class JpaAnswerReportRepositoryFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaCategoryRepository categoryRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaQuestionRepository questionRepository; + + @Autowired + private JpaAnswerRepository answerRepository; + + @Autowired + private JpaAnswerReportRepository answerReportRepository; + + protected User 신고자; + protected Answer 답변; + protected String 신고_내용 = "신고합니다."; + protected Answer 이미_신고된_답변; + protected AnswerReport 답변_신고1; + protected AnswerReport 답변_신고2; + protected AnswerReport 답변_신고3; + protected AnswerReport 답변_신고4; + + @BeforeEach + void setUp() { + final ProfileImage 프로필_이미지 = new ProfileImage("프로필.jpg", "프로필.jpg"); + final User 판매자 = User.builder() + .name("판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + final User 질문자 = User.builder() + .name("질문자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + 신고자 = User.builder() + .name("신고자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + final User 신고자2 = User.builder() + .name("신고자2") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12348") + .build(); + final User 신고자3 = User.builder() + .name("신고자3") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12349") + .build(); + final User 신고자4 = User.builder() + .name("신고자4") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12350") + .build(); + + final Category 전자기기_카테고리 = new Category("전자기기"); + final Category 전자기기_서브_노트북_카테고리 = new Category("노트북 카테고리"); + 전자기기_카테고리.addSubCategory(전자기기_서브_노트북_카테고리); + final AuctionImage 경매_이미지 = new AuctionImage("경매이미지.jpg", "경매이미지.jpg"); + final Auction 경매 = Auction.builder() + .seller(판매자) + .title("경매 상품") + .description("이것은 경매 상품입니다.") + .subCategory(전자기기_서브_노트북_카테고리) + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 경매.addAuctionImages(List.of(경매_이미지)); + + final Question 질문1 = new Question(경매, 질문자, "질문드립니다."); + final Question 질문2 = new Question(경매, 질문자, "질문드립니다."); + 답변 = new Answer("답변드립니다."); + 이미_신고된_답변 = new Answer("답변드립니다."); + 질문1.addAnswer(답변); + 질문2.addAnswer(이미_신고된_답변); + + 답변_신고1 = new AnswerReport(신고자, 이미_신고된_답변, "신고합니다."); + 답변_신고2 = new AnswerReport(신고자2, 이미_신고된_답변, "신고합니다."); + 답변_신고3 = new AnswerReport(신고자3, 이미_신고된_답변, "신고합니다."); + 답변_신고4 = new AnswerReport(신고자4, 이미_신고된_답변, "신고합니다."); + + userRepository.saveAll(List.of(판매자, 질문자, 신고자, 신고자2, 신고자3, 신고자4)); + + categoryRepository.saveAll(List.of(전자기기_카테고리, 전자기기_서브_노트북_카테고리)); + auctionRepository.save(경매); + + questionRepository.saveAll(List.of(질문1, 질문2)); + answerRepository.saveAll(List.of(답변, 이미_신고된_답변)); + answerReportRepository.saveAll(List.of(답변_신고1, 답변_신고2, 답변_신고3, 답변_신고4)); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/fixture/JpaAuctionReportRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/fixture/JpaAuctionReportRepositoryFixture.java new file mode 100644 index 000000000..79388dd15 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/fixture/JpaAuctionReportRepositoryFixture.java @@ -0,0 +1,119 @@ +package com.ddang.ddang.report.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.image.infrastructure.persistence.JpaAuctionImageRepository; +import com.ddang.ddang.image.infrastructure.persistence.JpaProfileImageRepository; +import com.ddang.ddang.report.domain.AuctionReport; +import com.ddang.ddang.report.infrastructure.persistence.JpaAuctionReportRepository; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class JpaAuctionReportRepositoryFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaCategoryRepository categoryRepository; + + @Autowired + private JpaProfileImageRepository profileImageRepository; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaAuctionImageRepository auctionImageRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaAuctionReportRepository auctionReportRepository; + + protected Long 존재하지_않는_경매_아이디 = -9999L; + protected Long 존재하지_않는_사용자_아이디 = -9999L; + protected User 판매자; + protected User 신고자1; + protected Auction 경매; + protected AuctionReport 경매_신고1; + protected AuctionReport 경매_신고2; + protected AuctionReport 경매_신고3; + + @BeforeEach + void setUp() { + final ProfileImage 프로필_이미지 = new ProfileImage("프로필.jpg", "프로필.jpg"); + 판매자 = User.builder() + .name("판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + 신고자1 = User.builder() + .name("신고자1") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + final User 신고자2 = User.builder() + .name("신고자2") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + final User 신고자3 = User.builder() + .name("신고자3") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12348") + .build(); + + final Category 전자기기_카테고리 = new Category("전자기기"); + final Category 전자기기_서브_노트북_카테고리 = new Category("노트북 카테고리"); + 전자기기_카테고리.addSubCategory(전자기기_서브_노트북_카테고리); + final AuctionImage 경매_이미지 = new AuctionImage("경매이미지.jpg", "경매이미지.jpg"); + 경매 = Auction.builder() + .seller(판매자) + .title("경매 상품") + .description("이것은 경매 상품입니다.") + .subCategory(전자기기_서브_노트북_카테고리) + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 경매.addAuctionImages(List.of(경매_이미지)); + + 경매_신고1 = new AuctionReport(신고자1, 경매, "신고합니다"); + 경매_신고2 = new AuctionReport(신고자2, 경매, "신고합니다"); + 경매_신고3 = new AuctionReport(신고자3, 경매, "신고합니다"); + + + profileImageRepository.save(프로필_이미지); + userRepository.saveAll(List.of(판매자, 신고자1, 신고자2, 신고자3)); + + categoryRepository.saveAll(List.of(전자기기_카테고리, 전자기기_서브_노트북_카테고리)); + auctionImageRepository.save(경매_이미지); + auctionRepository.save(경매); + + auctionReportRepository.saveAll(List.of(경매_신고1, 경매_신고2, 경매_신고3)); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/fixture/JpaChatRoomReportRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/fixture/JpaChatRoomReportRepositoryFixture.java new file mode 100644 index 000000000..1e2cb92aa --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/fixture/JpaChatRoomReportRepositoryFixture.java @@ -0,0 +1,150 @@ +package com.ddang.ddang.report.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.image.infrastructure.persistence.JpaAuctionImageRepository; +import com.ddang.ddang.image.infrastructure.persistence.JpaProfileImageRepository; +import com.ddang.ddang.report.domain.ChatRoomReport; +import com.ddang.ddang.report.infrastructure.persistence.JpaChatRoomReportRepository; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class JpaChatRoomReportRepositoryFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaCategoryRepository categoryRepository; + + @Autowired + private JpaProfileImageRepository profileImageRepository; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaAuctionImageRepository auctionImageRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaChatRoomReportRepository chatRoomReportRepository; + + @Autowired + private JpaChatRoomRepository chatRoomRepository; + + protected Long 존재하지_않는_채팅방_아이디 = -9999L; + protected Long 존재하지_않는_사용자_아이디 = -9999L; + protected User 구매자1; + protected User 구매자1겸_신고자; + protected ChatRoom 채팅방1; + protected ChatRoomReport 채팅방_신고1; + protected ChatRoomReport 채팅방_신고2; + protected ChatRoomReport 채팅방_신고3; + + @BeforeEach + void setUp() { + final ProfileImage 프로필_이미지 = new ProfileImage("프로필.jpg", "프로필.jpg"); + final User 판매자 = User.builder() + .name("판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + 구매자1 = User.builder() + .name("구매자1") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + final User 구매자2겸_신고자 = User.builder() + .name("구매자2") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + final User 구매자3겸_신고자 = User.builder() + .name("구매자3") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12348") + .build(); + 구매자1겸_신고자 = 구매자1; + + final Category 전자기기_카테고리 = new Category("전자기기"); + final Category 전자기기_서브_노트북_카테고리 = new Category("노트북 카테고리"); + 전자기기_카테고리.addSubCategory(전자기기_서브_노트북_카테고리); + final AuctionImage 경매_이미지 = new AuctionImage("경매이미지.jpg", "경매이미지.jpg"); + final Auction 경매1 = Auction.builder() + .seller(판매자) + .title("경매 상품") + .description("이것은 경매 상품입니다.") + .subCategory(전자기기_서브_노트북_카테고리) + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 경매1.addAuctionImages(List.of(경매_이미지)); + final Auction 경매2 = Auction.builder() + .seller(판매자) + .title("경매 상품") + .description("이것은 경매 상품입니다.") + .subCategory(전자기기_서브_노트북_카테고리) + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 경매2.addAuctionImages(List.of(경매_이미지)); + final Auction 경매3 = Auction.builder() + .seller(판매자) + .title("경매 상품") + .description("이것은 경매 상품입니다.") + .subCategory(전자기기_서브_노트북_카테고리) + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 경매3.addAuctionImages(List.of(경매_이미지)); + + 채팅방1 = new ChatRoom(경매1, 구매자1); + final ChatRoom 채팅방2 = new ChatRoom(경매2, 구매자2겸_신고자); + final ChatRoom 채팅방3 = new ChatRoom(경매3, 구매자3겸_신고자); + + 채팅방_신고1 = new ChatRoomReport(구매자1겸_신고자, 채팅방1, "신고합니다."); + 채팅방_신고2 = new ChatRoomReport(구매자2겸_신고자, 채팅방2, "신고합니다."); + 채팅방_신고3 = new ChatRoomReport(구매자3겸_신고자, 채팅방3, "신고합니다."); + + profileImageRepository.save(프로필_이미지); + userRepository.saveAll(List.of(판매자, 구매자1, 구매자2겸_신고자, 구매자3겸_신고자)); + + categoryRepository.saveAll(List.of(전자기기_카테고리, 전자기기_서브_노트북_카테고리)); + auctionImageRepository.save(경매_이미지); + auctionRepository.saveAll(List.of(경매1, 경매2, 경매3)); + + chatRoomRepository.saveAll(List.of(채팅방1, 채팅방2, 채팅방3)); + + chatRoomReportRepository.saveAll(List.of(채팅방_신고1, 채팅방_신고2, 채팅방_신고3)); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/fixture/JpaQuestionReportRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/fixture/JpaQuestionReportRepositoryFixture.java new file mode 100644 index 000000000..cae501164 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/report/infrastructure/persistence/fixture/JpaQuestionReportRepositoryFixture.java @@ -0,0 +1,130 @@ +package com.ddang.ddang.report.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.qna.domain.Question; +import com.ddang.ddang.qna.infrastructure.JpaQuestionRepository; +import com.ddang.ddang.report.domain.QuestionReport; +import com.ddang.ddang.report.infrastructure.persistence.JpaQuestionReportRepository; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class JpaQuestionReportRepositoryFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaCategoryRepository categoryRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaQuestionRepository questionRepository; + + @Autowired + private JpaQuestionReportRepository questionReportRepository; + + protected User 신고자; + protected Question 질문; + protected Question 이미_신고한_질문; + protected String 신고_내용 = "신고합니다."; + protected QuestionReport 질문_신고1; + protected QuestionReport 질문_신고2; + protected QuestionReport 질문_신고3; + protected QuestionReport 질문_신고4; + + @BeforeEach + void setUp() { + final ProfileImage 프로필_이미지 = new ProfileImage("프로필.jpg", "프로필.jpg"); + final User 판매자 = User.builder() + .name("판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + final User 질문자 = User.builder() + .name("질문자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + 신고자 = User.builder() + .name("신고자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + final User 신고자2 = User.builder() + .name("신고자2") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12348") + .build(); + final User 신고자3 = User.builder() + .name("신고자3") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12349") + .build(); + final User 신고자4 = User.builder() + .name("신고자4") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12350") + .build(); + + final Category 전자기기_카테고리 = new Category("전자기기"); + final Category 전자기기_서브_노트북_카테고리 = new Category("노트북 카테고리"); + 전자기기_카테고리.addSubCategory(전자기기_서브_노트북_카테고리); + final AuctionImage 경매_이미지 = new AuctionImage("경매이미지.jpg", "경매이미지.jpg"); + final Auction 경매 = Auction.builder() + .seller(판매자) + .title("경매 상품") + .description("이것은 경매 상품입니다.") + .subCategory(전자기기_서브_노트북_카테고리) + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 경매.addAuctionImages(List.of(경매_이미지)); + + 질문 = new Question(경매, 질문자, "질문드립니다."); + 이미_신고한_질문 = new Question(경매, 질문자, "질문드립니다."); + + 질문_신고1 = new QuestionReport(신고자, 이미_신고한_질문, "신고합니다"); + 질문_신고2 = new QuestionReport(신고자2, 이미_신고한_질문, "신고합니다"); + 질문_신고3 = new QuestionReport(신고자3, 이미_신고한_질문, "신고합니다"); + 질문_신고4 = new QuestionReport(신고자4, 이미_신고한_질문, "신고합니다"); + + userRepository.saveAll(List.of(판매자, 질문자, 신고자, 신고자2, 신고자3, 신고자4)); + + categoryRepository.saveAll(List.of(전자기기_카테고리, 전자기기_서브_노트북_카테고리)); + auctionRepository.save(경매); + + questionRepository.saveAll(List.of(질문, 이미_신고한_질문)); + questionReportRepository.saveAll(List.of(질문_신고1, 질문_신고2, 질문_신고3, 질문_신고4)); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/report/presentation/ReportControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/report/presentation/ReportControllerTest.java index daf7ba855..75b56549e 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/report/presentation/ReportControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/report/presentation/ReportControllerTest.java @@ -1,62 +1,47 @@ package com.ddang.ddang.report.presentation; import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; -import com.ddang.ddang.authentication.application.AuthenticationUserService; -import com.ddang.ddang.authentication.application.BlackListTokenService; import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; import com.ddang.ddang.authentication.domain.TokenDecoder; import com.ddang.ddang.authentication.domain.TokenType; import com.ddang.ddang.authentication.domain.dto.AuthenticationStore; -import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims; import com.ddang.ddang.chat.application.exception.ChatRoomNotFoundException; -import com.ddang.ddang.configuration.RestDocsConfiguration; import com.ddang.ddang.exception.GlobalExceptionHandler; -import com.ddang.ddang.report.application.AuctionReportService; -import com.ddang.ddang.report.application.ChatRoomReportService; +import com.ddang.ddang.qna.application.exception.AnswerNotFoundException; +import com.ddang.ddang.qna.application.exception.InvalidAnswererException; +import com.ddang.ddang.qna.application.exception.QuestionNotFoundException; +import com.ddang.ddang.report.application.dto.CreateAnswerReportDto; import com.ddang.ddang.report.application.dto.CreateAuctionReportDto; import com.ddang.ddang.report.application.dto.CreateChatRoomReportDto; -import com.ddang.ddang.report.application.dto.ReadAuctionInReportDto; -import com.ddang.ddang.report.application.dto.ReadAuctionReportDto; -import com.ddang.ddang.report.application.dto.ReadChatRoomInReportDto; -import com.ddang.ddang.report.application.dto.ReadChatRoomReportDto; -import com.ddang.ddang.report.application.dto.ReadReporterDto; -import com.ddang.ddang.report.application.dto.ReadUserInReportDto; +import com.ddang.ddang.report.application.dto.CreateQuestionReportDto; import com.ddang.ddang.report.application.exception.AlreadyReportAuctionException; import com.ddang.ddang.report.application.exception.AlreadyReportChatRoomException; -import com.ddang.ddang.report.application.exception.ChatRoomReportNotAccessibleException; +import com.ddang.ddang.report.application.exception.InvalidChatRoomReportException; +import com.ddang.ddang.report.application.exception.InvalidQuestionReportException; import com.ddang.ddang.report.application.exception.InvalidReportAuctionException; import com.ddang.ddang.report.application.exception.InvalidReporterToAuctionException; +import com.ddang.ddang.report.presentation.dto.request.CreateAnswerReportRequest; import com.ddang.ddang.report.presentation.dto.request.CreateAuctionReportRequest; import com.ddang.ddang.report.presentation.dto.request.CreateChatRoomReportRequest; +import com.ddang.ddang.report.presentation.dto.request.CreateQuestionReportRequest; +import com.ddang.ddang.report.presentation.fixture.ReportControllerFixture; import com.ddang.ddang.user.application.exception.UserNotFoundException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; -import org.springframework.context.annotation.Import; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import java.time.LocalDateTime; import java.util.List; import java.util.Optional; +import java.util.stream.Stream; import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; @@ -76,52 +61,22 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(controllers = {ReportController.class}, - excludeFilters = { - @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfigurer.class), - @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com\\.ddang\\.ddang\\.authentication\\.configuration\\..*") - } -) -@AutoConfigureRestDocs -@Import(RestDocsConfiguration.class) -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class ReportControllerTest { +class ReportControllerTest extends ReportControllerFixture { - @MockBean - AuctionReportService auctionReportService; - - @MockBean - ChatRoomReportService chatRoomReportService; - - @MockBean - BlackListTokenService blackListTokenService; - - @MockBean - AuthenticationUserService authenticationUserService; - - @Autowired - ReportController reportController; - - @Autowired - RestDocumentationResultHandler restDocs; - - @Autowired - ObjectMapper objectMapper; - - TokenDecoder mockTokenDecoder; + TokenDecoder tokenDecoder; MockMvc mockMvc; @BeforeEach - void setUp(@Autowired RestDocumentationContextProvider provider) { - mockTokenDecoder = mock(TokenDecoder.class); + void setUp() { + tokenDecoder = mock(TokenDecoder.class); final AuthenticationStore store = new AuthenticationStore(); final AuthenticationInterceptor interceptor = new AuthenticationInterceptor( blackListTokenService, authenticationUserService, - mockTokenDecoder, + tokenDecoder, store ); final AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(store); @@ -139,203 +94,163 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { @Test void 경매_신고를_등록한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateAuctionReportRequest createAuctionReportRequest = new CreateAuctionReportRequest(1L, "신고합니다"); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(auctionReportService.create(any(CreateAuctionReportDto.class))).willReturn(1L); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(auctionReportService.create(any(CreateAuctionReportDto.class))).willReturn(생성된_경매_신고_아이디); // when & then - mockMvc.perform(post("/reports/auctions") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(createAuctionReportRequest)) - ) - .andExpectAll( - status().isCreated(), - header().string(HttpHeaders.LOCATION, is("/auctions/1")) - ) - .andDo( - restDocs.document( - requestHeaders( - headerWithName("Authorization").description("회원 Bearer 인증 정보") - ), - requestFields( - fieldWithPath("auctionId").description("신고할 경매 ID"), - fieldWithPath("description").description("신고 내용") - ) - ) - ); + final ResultActions resultActions = mockMvc.perform(post("/reports/auctions") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(경매_신고_request)) + ) + .andExpectAll( + status().isCreated(), + header().string(HttpHeaders.LOCATION, is("/auctions/1")) + ); + + createAuctionReport_문서화(resultActions); } @Test void 해당_사용자가_없는_경우_신고시_404를_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateAuctionReportRequest createAuctionReportRequest = new CreateAuctionReportRequest(1L, "신고합니다"); - final UserNotFoundException userNotFoundException = new UserNotFoundException("해당 사용자를 찾을 수 없습니다."); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(auctionReportService.create(any(CreateAuctionReportDto.class))).willThrow(userNotFoundException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(존재하지_않는_사용자_ID_클레임)); + given(auctionReportService.create(any(CreateAuctionReportDto.class))) + .willThrow(new UserNotFoundException("해당 사용자를 찾을 수 없습니다.")); // when & then mockMvc.perform(post("/reports/auctions") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(createAuctionReportRequest)) + .content(objectMapper.writeValueAsString(경매_신고_request)) ) .andExpectAll( status().isNotFound(), - jsonPath("$.message", is(userNotFoundException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 해당_경매가_없는_경우_신고시_404를_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateAuctionReportRequest createAuctionReportRequest = new CreateAuctionReportRequest(1L, "신고합니다"); - final AuctionNotFoundException auctionNotFoundException = new AuctionNotFoundException("해당 경매를 찾을 수 없습니다."); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(auctionReportService.create(any(CreateAuctionReportDto.class))).willThrow(auctionNotFoundException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(auctionReportService.create(any(CreateAuctionReportDto.class))) + .willThrow(new AuctionNotFoundException("해당 경매를 찾을 수 없습니다.")); // when & then mockMvc.perform(post("/reports/auctions") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(createAuctionReportRequest)) + .content(objectMapper.writeValueAsString(경매_신고_request)) ) .andExpectAll( status().isNotFound(), - jsonPath("$.message", is(auctionNotFoundException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 본인이_등록한_경매를_신고할_경우_400을_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateAuctionReportRequest createAuctionReportRequest = new CreateAuctionReportRequest(1L, "신고합니다"); - final InvalidReporterToAuctionException invalidReporterToAuctionException = new InvalidReporterToAuctionException("본인 경매글입니다."); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(auctionReportService.create(any(CreateAuctionReportDto.class))).willThrow(invalidReporterToAuctionException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(auctionReportService.create(any(CreateAuctionReportDto.class))) + .willThrow(new InvalidReporterToAuctionException("본인 경매글입니다.")); // when & then mockMvc.perform(post("/reports/auctions") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(createAuctionReportRequest)) + .content(objectMapper.writeValueAsString(경매_신고_request)) ) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is(invalidReporterToAuctionException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 이미_삭제된_경매_신고시_400을_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateAuctionReportRequest createAuctionReportRequest = new CreateAuctionReportRequest(1L, "신고합니다"); - final InvalidReportAuctionException invalidReportAuctionException = new InvalidReportAuctionException("이미 삭제된 경매입니다."); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(auctionReportService.create(any(CreateAuctionReportDto.class))).willThrow(invalidReportAuctionException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(auctionReportService.create(any(CreateAuctionReportDto.class))) + .willThrow(new InvalidReportAuctionException("이미 삭제된 경매입니다.")); // when & then mockMvc.perform(post("/reports/auctions") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(createAuctionReportRequest)) + .content(objectMapper.writeValueAsString(경매_신고_request)) ) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is(invalidReportAuctionException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 이미_신고한_경매_신고시_400을_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateAuctionReportRequest createAuctionReportRequest = new CreateAuctionReportRequest(1L, "신고합니다"); - final AlreadyReportAuctionException alreadyReportAuctionException = new AlreadyReportAuctionException("이미 신고한 경매입니다."); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(auctionReportService.create(any(CreateAuctionReportDto.class))).willThrow(alreadyReportAuctionException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(auctionReportService.create(any(CreateAuctionReportDto.class))) + .willThrow(new AlreadyReportAuctionException("이미 신고한 경매입니다.")); // when & then mockMvc.perform(post("/reports/auctions") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(createAuctionReportRequest)) + .content(objectMapper.writeValueAsString(경매_신고_request)) ) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is(alreadyReportAuctionException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 경매_아이디가_없는_경우_신고시_400을_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateAuctionReportRequest createAuctionReportRequest = new CreateAuctionReportRequest(null, "신고합니다"); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(auctionReportService.create(any(CreateAuctionReportDto.class))).willReturn(1L); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); // when & then mockMvc.perform(post("/reports/auctions") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(createAuctionReportRequest)) + .content(objectMapper.writeValueAsString(경매_아이디가_없는_신고_request)) ) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is("경매 아이디가 입력되지 않았습니다.")) + jsonPath("$.message").exists() ); } @Test void 경매_아이디가_음수인_경우_신고시_400을_반환한다() throws Exception { // given - final Long invalidAuctionId = -999L; - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateAuctionReportRequest createAuctionReportRequest = new CreateAuctionReportRequest(invalidAuctionId, "신고합니다"); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(auctionReportService.create(any(CreateAuctionReportDto.class))).willReturn(1L); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); // when & then mockMvc.perform(post("/reports/auctions") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(createAuctionReportRequest)) + .content(objectMapper.writeValueAsString(경매_아이디가_음수인_신고_request)) ) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is("경매 아이디는 양수여야 합니다.")) + jsonPath("$.message").exists() ); } @ParameterizedTest - @NullAndEmptySource - void 신고_내용_없이_경매_신고시_400을_반환한다(final String description) throws Exception { + @MethodSource("provideAuctionReportRequestWithEmptyDescription") + void 신고_내용_없이_경매_신고시_400을_반환한다(final CreateAuctionReportRequest 내용이_없는_경매_신고_요청) throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateAuctionReportRequest createAuctionReportRequest = new CreateAuctionReportRequest(1L, description); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(auctionReportService.create(any(CreateAuctionReportDto.class))).willReturn(1L); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); // when & then mockMvc.perform(post("/reports/auctions") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(createAuctionReportRequest)) + .content(objectMapper.writeValueAsString(내용이_없는_경매_신고_요청)) ) .andExpectAll( status().isBadRequest(), @@ -343,362 +258,832 @@ void setUp(@Autowired RestDocumentationContextProvider provider) { ); } + private static Stream provideAuctionReportRequestWithEmptyDescription() { + return Stream.of(신고_내용이_null인_경매_신고_request, 신고_내용이_빈값인_경매_신고_request); + } + @Test void 전체_경매_신고_목록을_조회한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - - final ReadUserInReportDto userDto = new ReadUserInReportDto(1L, "판매자", "profile.png", 4.0d, "12345"); - final ReadAuctionInReportDto auctionDto = new ReadAuctionInReportDto( - 1L, - userDto, - "제목", - "설명", - 100, - 1_00, - false, - LocalDateTime.now().plusDays(2), - 2 - ); - final ReadAuctionReportDto auctionReportDto1 = new ReadAuctionReportDto( - 1L, - new ReadReporterDto(1L, "회원1", "이미지1", 5.0), - LocalDateTime.now(), - auctionDto, - "신고합니다." - ); - final ReadAuctionReportDto auctionReportDto2 = new ReadAuctionReportDto( - 2L, - new ReadReporterDto(2L, "회원2", "이미지2", 5.0), - LocalDateTime.now(), - auctionDto, - "신고합니다." - ); - final ReadAuctionReportDto auctionReportDto3 = new ReadAuctionReportDto( - 3L, - new ReadReporterDto(3L, "회원3", "이미지3", 5.0), - LocalDateTime.now(), - auctionDto, - "신고합니다." - ); - given(auctionReportService.readAll()) - .willReturn(List.of(auctionReportDto1, auctionReportDto2, auctionReportDto3)); - - // when & then - mockMvc.perform(get("/reports/auctions") - .contentType(MediaType.APPLICATION_JSON) - ) - .andExpectAll( - status().isOk(), - jsonPath("$.reports.[0].id", is(auctionReportDto1.id()), Long.class), - jsonPath("$.reports.[0].reporter.id", is(auctionReportDto1.reporterDto().id()), Long.class), - jsonPath("$.reports.[0].reporter.name", is(auctionReportDto1.reporterDto().name())), - jsonPath("$.reports.[0].createdTime").exists(), - jsonPath("$.reports.[0].auction.id", is(auctionReportDto1.auctionDto().id()), Long.class), - jsonPath("$.reports.[0].auction.title", is(auctionReportDto1.auctionDto().title())), - jsonPath("$.reports.[0].description", is(auctionReportDto1.description())), - jsonPath("$.reports.[1].id", is(auctionReportDto2.id()), Long.class), - jsonPath("$.reports.[1].reporter.id", is(auctionReportDto2.reporterDto().id()), Long.class), - jsonPath("$.reports.[1].reporter.name", is(auctionReportDto2.reporterDto().name())), - jsonPath("$.reports.[1].createdTime").exists(), - jsonPath("$.reports.[1].auction.id", is(auctionReportDto2.auctionDto().id()), Long.class), - jsonPath("$.reports.[1].auction.title", is(auctionReportDto2.auctionDto().title())), - jsonPath("$.reports.[1].description", is(auctionReportDto2.description())), - jsonPath("$.reports.[2].id", is(auctionReportDto3.id()), Long.class), - jsonPath("$.reports.[2].reporter.id", is(auctionReportDto3.reporterDto().id()), Long.class), - jsonPath("$.reports.[2].reporter.name", is(auctionReportDto3.reporterDto().name())), - jsonPath("$.reports.[2].createdTime").exists(), - jsonPath("$.reports.[2].auction.id", is(auctionReportDto3.auctionDto().id()), Long.class), - jsonPath("$.reports.[2].auction.title", is(auctionReportDto3.auctionDto().title())), - jsonPath("$.reports.[2].description", is(auctionReportDto3.description())) - ) - .andDo( - restDocs.document( - responseFields( - fieldWithPath("reports.[]").type(JsonFieldType.ARRAY).description("모든 경매 신고 목록"), - fieldWithPath("reports.[].id").type(JsonFieldType.NUMBER).description("경매 신고 글 ID"), - fieldWithPath("reports.[].reporter.id").type(JsonFieldType.NUMBER).description("경매 신고한 사용자의 ID"), - fieldWithPath("reports.[].reporter.name").type(JsonFieldType.STRING).description("경매 신고한 사용자의 이름"), - fieldWithPath("reports.[].createdTime").type(JsonFieldType.STRING).description("경매 신고 시간"), - fieldWithPath("reports.[].auction.id").type(JsonFieldType.NUMBER).description("신고한 경매 ID"), - fieldWithPath("reports.[].auction.title").type(JsonFieldType.STRING).description("신고한 경매 제목"), - fieldWithPath("reports.[].description").type(JsonFieldType.STRING).description("신고 내용") - ) + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(auctionReportService.readAll()).willReturn(List.of(경매_신고_dto1, 경매_신고_dto2, 경매_신고_dto3)); + + // when & then + final ResultActions resultActions = + mockMvc.perform(get("/reports/auctions") + .contentType(MediaType.APPLICATION_JSON) ) - ); + .andExpectAll( + status().isOk(), + jsonPath("$.reports.[0].id", is(경매_신고_dto1.id()), Long.class), + jsonPath("$.reports.[0].reporter.id", is(경매_신고_dto1.reporterDto().id()), Long.class), + jsonPath("$.reports.[0].reporter.name", is(경매_신고_dto1.reporterDto().name())), + jsonPath("$.reports.[0].createdTime").exists(), + jsonPath("$.reports.[0].auction.id", is(경매_신고_dto1.auctionDto().id()), Long.class), + jsonPath("$.reports.[0].auction.title", is(경매_신고_dto1.auctionDto().title())), + jsonPath("$.reports.[0].description", is(경매_신고_dto1.description())), + jsonPath("$.reports.[1].id", is(경매_신고_dto2.id()), Long.class), + jsonPath("$.reports.[1].reporter.id", is(경매_신고_dto2.reporterDto().id()), Long.class), + jsonPath("$.reports.[1].reporter.name", is(경매_신고_dto2.reporterDto().name())), + jsonPath("$.reports.[1].createdTime").exists(), + jsonPath("$.reports.[1].auction.id", is(경매_신고_dto2.auctionDto().id()), Long.class), + jsonPath("$.reports.[1].auction.title", is(경매_신고_dto2.auctionDto().title())), + jsonPath("$.reports.[1].description", is(경매_신고_dto2.description())), + jsonPath("$.reports.[2].id", is(경매_신고_dto3.id()), Long.class), + jsonPath("$.reports.[2].reporter.id", is(경매_신고_dto3.reporterDto().id()), Long.class), + jsonPath("$.reports.[2].reporter.name", is(경매_신고_dto3.reporterDto().name())), + jsonPath("$.reports.[2].createdTime").exists(), + jsonPath("$.reports.[2].auction.id", is(경매_신고_dto3.auctionDto().id()), Long.class), + jsonPath("$.reports.[2].auction.title", is(경매_신고_dto3.auctionDto().title())), + jsonPath("$.reports.[2].description", is(경매_신고_dto3.description())) + ); + + readAllAuctionReport_문서화(resultActions); } @Test void 채팅방_신고를_등록한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateChatRoomReportRequest createChatRoomReportRequest = new CreateChatRoomReportRequest(1L, "신고합니다"); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(chatRoomReportService.create(any(CreateChatRoomReportDto.class))).willReturn(1L); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(chatRoomReportService.create(any(CreateChatRoomReportDto.class))).willReturn(생성된_채팅방_신고_아이디); // when & then - mockMvc.perform(post("/reports/chat-rooms") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(createChatRoomReportRequest)) - ) - .andExpectAll( - status().isCreated(), - header().string(HttpHeaders.LOCATION, is("/chattings/1")) - ) - .andDo( - restDocs.document( - requestHeaders( - headerWithName("Authorization").description("회원 Bearer 인증 정보") - ), - requestFields( - fieldWithPath("chatRoomId").description("신고할 채팅방 ID"), - fieldWithPath("description").description("신고 내용") - ) - ) - ); + final ResultActions resultActions = mockMvc.perform(post("/reports/chat-rooms") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(채팅방_신고_request)) + ) + .andExpectAll( + status().isCreated(), + header().string(HttpHeaders.LOCATION, is("/chattings/1")) + ); + + createChatRoomReport_문서화(resultActions); } @Test void 존재하지_않은_사용자가_채팅방을_신고할시_404를_반환한다() throws Exception { // given - final Long invalidUserId = -999L; - final PrivateClaims privateClaims = new PrivateClaims(invalidUserId); - final CreateChatRoomReportRequest createChatRoomReportRequest = new CreateChatRoomReportRequest(1L, "신고합니다"); - final UserNotFoundException userNotFoundException = new UserNotFoundException("해당 사용자를 찾을 수 없습니다."); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(chatRoomReportService.create(any(CreateChatRoomReportDto.class))).willThrow(userNotFoundException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(존재하지_않는_사용자_ID_클레임)); + given(chatRoomReportService.create(any(CreateChatRoomReportDto.class))) + .willThrow(new UserNotFoundException("해당 사용자를 찾을 수 없습니다.")); // when & then mockMvc.perform(post("/reports/chat-rooms") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(createChatRoomReportRequest)) + .content(objectMapper.writeValueAsString(채팅방_신고_request)) ) .andExpectAll( status().isNotFound(), - jsonPath("$.message", is(userNotFoundException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 존재하지_않은_채팅방을_신고할시_404를_반환한다() throws Exception { // given - final Long invalidChatRoomId = 999L; - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateChatRoomReportRequest createChatRoomReportRequest = new CreateChatRoomReportRequest(invalidChatRoomId, "신고합니다"); - final ChatRoomNotFoundException chatRoomNotFoundException = new ChatRoomNotFoundException("해당 채팅방을 찾을 수 없습니다."); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(chatRoomReportService.create(any(CreateChatRoomReportDto.class))).willThrow(chatRoomNotFoundException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(chatRoomReportService.create(any(CreateChatRoomReportDto.class))) + .willThrow(new ChatRoomNotFoundException("해당 채팅방을 찾을 수 없습니다.")); // when & then mockMvc.perform(post("/reports/chat-rooms") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(createChatRoomReportRequest)) + .content(objectMapper.writeValueAsString(존재하지_않는_채팅방_신고_request)) ) .andExpectAll( status().isNotFound(), - jsonPath("$.message", is(chatRoomNotFoundException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 판매자_혹은_구매자가_아닌_사용자가_채팅방을_신고할시_403을_반환한다() throws Exception { // given - final Long unaccessibleUserId = 999L; - final PrivateClaims privateClaims = new PrivateClaims(unaccessibleUserId); - final CreateChatRoomReportRequest createChatRoomReportRequest = new CreateChatRoomReportRequest(1L, "신고합니다"); - final ChatRoomReportNotAccessibleException chatRoomReportNotAccessibleException = new ChatRoomReportNotAccessibleException("해당 채팅방을 신고할 권한이 없습니다."); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(chatRoomReportService.create(any(CreateChatRoomReportDto.class))).willThrow(chatRoomReportNotAccessibleException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(채팅방_참여자가_아닌_사용자_ID_클레임)); + given(chatRoomReportService.create(any(CreateChatRoomReportDto.class))) + .willThrow(new InvalidChatRoomReportException("해당 채팅방을 신고할 권한이 없습니다.")); // when & then mockMvc.perform(post("/reports/chat-rooms") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(createChatRoomReportRequest)) + .content(objectMapper.writeValueAsString(채팅방_신고_request)) ) .andExpectAll( status().isForbidden(), - jsonPath("$.message", is(chatRoomReportNotAccessibleException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 이미_신고한_사용자가_동일_채팅방을_신고할시_400을_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateChatRoomReportRequest createChatRoomReportRequest = new CreateChatRoomReportRequest(1L, "신고합니다"); - final AlreadyReportChatRoomException alreadyReportChatRoomException = new AlreadyReportChatRoomException("이미 신고한 채팅방입니다."); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(chatRoomReportService.create(any(CreateChatRoomReportDto.class))).willThrow(alreadyReportChatRoomException); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(chatRoomReportService.create(any(CreateChatRoomReportDto.class))) + .willThrow(new AlreadyReportChatRoomException("이미 신고한 채팅방입니다.")); // when & then mockMvc.perform(post("/reports/chat-rooms") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(createChatRoomReportRequest)) + .content(objectMapper.writeValueAsString(채팅방_신고_request)) ) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is(alreadyReportChatRoomException.getMessage())) + jsonPath("$.message").exists() ); } @Test void 채팅방_아이디가_없는_경우_신고시_400을_반환한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateChatRoomReportRequest createChatRoomReportRequest = new CreateChatRoomReportRequest(null, "신고합니다"); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(chatRoomReportService.create(any(CreateChatRoomReportDto.class))).willReturn(1L); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); // when & then mockMvc.perform(post("/reports/chat-rooms") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(createChatRoomReportRequest)) + .content(objectMapper.writeValueAsString(채팅방_아이디가_null인_신고_request)) ) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is("채팅방 아이디가 입력되지 않았습니다.")) + jsonPath("$.message").exists() ); } @Test void 채팅방_아이디가_음수인_경우_신고시_400을_반환한다() throws Exception { // given - final Long invalidAuctionId = -999L; - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateChatRoomReportRequest createChatRoomReportRequest = new CreateChatRoomReportRequest(invalidAuctionId, "신고합니다"); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(chatRoomReportService.create(any(CreateChatRoomReportDto.class))).willReturn(1L); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); // when & then mockMvc.perform(post("/reports/chat-rooms") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(createChatRoomReportRequest)) + .content(objectMapper.writeValueAsString(채팅방_아이디가_음수인_신고_request)) ) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is("채팅방 아이디는 양수여야 합니다.")) + jsonPath("$.message").exists() ); } @ParameterizedTest - @NullAndEmptySource - void 신고_내용_없이_채팅방_신고시_400을_반환한다(final String description) throws Exception { + @MethodSource("provideChatRoomReportRequestWithEmptyDescription") + void 신고_내용_없이_채팅방_신고시_400을_반환한다(final CreateChatRoomReportRequest 채팅방_신고_요청) throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - final CreateChatRoomReportRequest createChatRoomReportRequest = new CreateChatRoomReportRequest(1L, description); - - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(chatRoomReportService.create(any(CreateChatRoomReportDto.class))).willReturn(1L); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); // when & then mockMvc.perform(post("/reports/chat-rooms") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(createChatRoomReportRequest)) + .content(objectMapper.writeValueAsString(채팅방_신고_요청)) ) .andExpectAll( status().isBadRequest(), - jsonPath("$.message", is("신고 내용이 입력되지 않았습니다.")) + jsonPath("$.message").exists() ); } + private static Stream provideChatRoomReportRequestWithEmptyDescription() { + return Stream.of(신고_내용이_null인_채팅_신고_request, 신고_내용이_빈값인_채팅_신고_request); + } + @Test void 전체_채팅방_신고_목록을_조회한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - - final ReadUserInReportDto sellerDto = new ReadUserInReportDto(1L, "판매자", "profile.png", 4.0d, "12345"); - final ReadAuctionInReportDto auctionInReportDto = new ReadAuctionInReportDto( - 1L, - sellerDto, - "제목", - "설명", - 100, - 1_00, - false, - LocalDateTime.now().plusDays(2), - 2 + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(chatRoomReportService.readAll()).willReturn(List.of(채팅방_신고_dto1, 채팅방_신고_dto2, 채팅방_신고_dto3)); + + // when & then + final ResultActions resultActions = + mockMvc.perform(get("/reports/chat-rooms") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpectAll( + status().isOk(), + jsonPath("$.reports.[0].id", is(채팅방_신고_dto1.id()), Long.class), + jsonPath("$.reports.[0].reporter.id", is(채팅방_신고_dto1.reporterDto().id()), Long.class), + jsonPath("$.reports.[0].reporter.name", is(채팅방_신고_dto1.reporterDto().name())), + jsonPath("$.reports.[0].createdTime").exists(), + jsonPath("$.reports.[0].chatRoom.id", is(채팅방_신고_dto1.chatRoomDto().id()), Long.class), + jsonPath("$.reports.[0].description", is(채팅방_신고_dto1.description())), + jsonPath("$.reports.[1].id", is(채팅방_신고_dto2.id()), Long.class), + jsonPath("$.reports.[1].reporter.id", is(채팅방_신고_dto2.reporterDto().id()), Long.class), + jsonPath("$.reports.[1].reporter.name", is(채팅방_신고_dto2.reporterDto().name())), + jsonPath("$.reports.[1].createdTime").exists(), + jsonPath("$.reports.[1].chatRoom.id", is(채팅방_신고_dto2.chatRoomDto().id()), Long.class), + jsonPath("$.reports.[1].description", is(채팅방_신고_dto2.description())), + jsonPath("$.reports.[2].id", is(채팅방_신고_dto3.id()), Long.class), + jsonPath("$.reports.[2].reporter.id", is(채팅방_신고_dto3.reporterDto().id()), Long.class), + jsonPath("$.reports.[2].reporter.name", is(채팅방_신고_dto3.reporterDto().name())), + jsonPath("$.reports.[2].createdTime").exists(), + jsonPath("$.reports.[2].chatRoom.id", is(채팅방_신고_dto3.chatRoomDto().id()), Long.class), + jsonPath("$.reports.[2].description", is(채팅방_신고_dto3.description())) + ); + + readAllChatRoomReport_문서화(resultActions); + } + + @Test + void 질문_신고를_등록한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(questionReportService.create(any(CreateQuestionReportDto.class))).willReturn(생성된_질문_신고_아이디); + + // when & then + final ResultActions resultActions = + mockMvc.perform(post("/reports/questions") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(질문_신고_request)) + ) + .andExpectAll( + status().isCreated(), + header().string(HttpHeaders.LOCATION, is("/auctions/1/questions")) + ); + + createQuestionReport_문서화(resultActions); + } + + @Test + void 존재하지_않은_사용자가_질문을_신고할시_404를_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(존재하지_않는_사용자_ID_클레임)); + given(questionReportService.create(any(CreateQuestionReportDto.class))) + .willThrow(new UserNotFoundException("해당 사용자를 찾을 수 없습니다.")); + + // when & then + mockMvc.perform(post("/reports/questions") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(질문_신고_request)) + ) + .andExpectAll( + status().isNotFound(), + jsonPath("$.message").exists() + ); + } + + @Test + void 존재하지_않은_질문을_신고할시_404를_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(questionReportService.create(any(CreateQuestionReportDto.class))) + .willThrow(new QuestionNotFoundException("해당 질문을 찾을 수 없습니다.")); + + // when & then + mockMvc.perform(post("/reports/questions") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(존재하지_않는_질문_신고_request)) + ) + .andExpectAll( + status().isNotFound(), + jsonPath("$.message").exists() + ); + } + + @Test + void 질문_작성자_본인이_자신의_질문을_신고할시_400을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(questionReportService.create(any(CreateQuestionReportDto.class))) + .willThrow(new InvalidQuestionReportException("본인 질문입니다.")); + + // when & then + mockMvc.perform(post("/reports/questions") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(본인의_질문_신고_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @Test + void 이미_신고한_사용자가_동일_질문을_신고할시_400을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(questionReportService.create(any(CreateQuestionReportDto.class))) + .willThrow(new InvalidQuestionReportException("이미 신고한 질문입니다.")); + + // when & then + mockMvc.perform(post("/reports/questions") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(이미_신고한_질문_신고_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @Test + void 경매_아이디가_없는_경우_질문_신고시_400을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + + // when & then + mockMvc.perform(post("/reports/questions") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(경매_아이디가_null인_질문_신고_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @Test + void 경매_아이디가_음수인_경우_질문_신고시_400을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + + // when & then + mockMvc.perform(post("/reports/questions") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(경매_아이디가_음수인_질문_신고_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @Test + void 질문_아이디가_없는_경우_질문_신고시_400을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + + // when & then + mockMvc.perform(post("/reports/questions") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(질문_아이디가_null인_질문_신고_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @Test + void 질문_아이디가_음수인_경우_질문_신고시_400을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + + // when & then + mockMvc.perform(post("/reports/questions") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(질문_아이디가_음수인_질문_신고_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @ParameterizedTest + @MethodSource("provideQuestionReportRequestWithEmptyDescription") + void 신고_내용_없이_질문_신고시_400을_반환한다(final CreateQuestionReportRequest 질문_신고_요청) throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + + // when & then + mockMvc.perform(post("/reports/questions") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(질문_신고_요청)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + private static Stream provideQuestionReportRequestWithEmptyDescription() { + return Stream.of(신고_내용이_null인_질문_신고_request, 신고_내용이_빈값인_질문_신고_request); + } + + @Test + void 전체_질문_신고_목록을_조회한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(questionReportService.readAll()).willReturn(List.of(질문_신고_dto1, 질문_신고_dto2, 질문_신고_dto3)); + + // when & then + final ResultActions resultActions = + mockMvc.perform(get("/reports/questions") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpectAll( + status().isOk(), + jsonPath("$.reports.[0].id", is(질문_신고_dto1.id()), Long.class), + jsonPath("$.reports.[0].reporter.id", is(질문_신고_dto1.reporterDto().id()), Long.class), + jsonPath("$.reports.[0].reporter.name", is(질문_신고_dto1.reporterDto().name())), + jsonPath("$.reports.[0].createdTime").exists(), + jsonPath("$.reports.[0].question.id", is(질문_신고_dto1.questionDto().id()), Long.class), + jsonPath("$.reports.[0].description", is(질문_신고_dto1.description())), + jsonPath("$.reports.[1].id", is(질문_신고_dto2.id()), Long.class), + jsonPath("$.reports.[1].reporter.id", is(질문_신고_dto2.reporterDto().id()), Long.class), + jsonPath("$.reports.[1].reporter.name", is(질문_신고_dto2.reporterDto().name())), + jsonPath("$.reports.[1].createdTime").exists(), + jsonPath("$.reports.[1].question.id", is(질문_신고_dto2.questionDto().id()), Long.class), + jsonPath("$.reports.[1].description", is(질문_신고_dto2.description())), + jsonPath("$.reports.[2].id", is(질문_신고_dto3.id()), Long.class), + jsonPath("$.reports.[2].reporter.id", is(질문_신고_dto3.reporterDto().id()), Long.class), + jsonPath("$.reports.[2].reporter.name", is(질문_신고_dto3.reporterDto().name())), + jsonPath("$.reports.[2].createdTime").exists(), + jsonPath("$.reports.[2].question.id", is(질문_신고_dto3.questionDto().id()), Long.class), + jsonPath("$.reports.[2].description", is(질문_신고_dto3.description())) + ); + + readAllQuestionReport_문서화(resultActions); + } + + @Test + void 답변_신고를_등록한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(answerReportService.create(any(CreateAnswerReportDto.class))).willReturn(생성된_답변_신고_아이디); + + // when & then + final ResultActions resultActions = + mockMvc.perform(post("/reports/answers") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(답변_신고_request)) + ) + .andExpectAll( + status().isCreated(), + header().string(HttpHeaders.LOCATION, is("/auctions/1/questions")) + ); + + createAnswerReport_문서화(resultActions); + } + + @Test + void 존재하지_않은_사용자가_답변을_신고할시_404를_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(존재하지_않는_사용자_ID_클레임)); + given(answerReportService.create(any(CreateAnswerReportDto.class))) + .willThrow(new UserNotFoundException("해당 사용자를 찾을 수 없습니다.")); + + // when & then + mockMvc.perform(post("/reports/answers") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(답변_신고_request)) + ) + .andExpectAll( + status().isNotFound(), + jsonPath("$.message").exists() + ); + } + + @Test + void 존재하지_않은_답변을_신고할시_404를_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(answerReportService.create(any(CreateAnswerReportDto.class))) + .willThrow(new AnswerNotFoundException("해당 질문을 찾을 수 없습니다.")); + + // when & then + mockMvc.perform(post("/reports/answers") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(존재하지_않는_답변_신고_request)) + ) + .andExpectAll( + status().isNotFound(), + jsonPath("$.message").exists() + ); + } + + @Test + void 질문_작성자_본인이_자신의_답변을_신고할시_400을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(answerReportService.create(any(CreateAnswerReportDto.class))) + .willThrow(new InvalidAnswererException("본인 질문입니다.")); + + // when & then + mockMvc.perform(post("/reports/answers") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(본인의_답변_신고_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @Test + void 이미_신고한_사용자가_동일_답변을_신고할시_400을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(answerReportService.create(any(CreateAnswerReportDto.class))) + .willThrow(new InvalidAnswererException("이미 신고한 질문입니다.")); + + // when & then + mockMvc.perform(post("/reports/answers") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(이미_신고한_답변_신고_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @Test + void 경매_아이디가_없는_경우_답변_신고시_400을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + + // when & then + mockMvc.perform(post("/reports/answers") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(경매_아이디가_null인_답변_신고_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @Test + void 경매_아이디가_음수인_경우_답변_신고시_400을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + + // when & then + mockMvc.perform(post("/reports/answers") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(경매_아이디가_음수인_답변_신고_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @Test + void 답변_아이디가_없는_경우_질문_신고시_400을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + + // when & then + mockMvc.perform(post("/reports/answers") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(질문_아이디가_null인_답변_신고_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @Test + void 질문_아이디가_음수인_경우_답변_신고시_400을_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + + // when & then + mockMvc.perform(post("/reports/answers") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(답변_아이디가_음수인_질문_신고_request)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @ParameterizedTest + @MethodSource("provideAnswerReportRequestWithEmptyDescription") + void 신고_내용_없이_답변_신고시_400을_반환한다(final CreateAnswerReportRequest 답변_신고_요청) throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + + // when & then + mockMvc.perform(post("/reports/answers") + .header(HttpHeaders.AUTHORIZATION, 엑세스_토큰_값) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(답변_신고_요청)) + ) + .andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + private static Stream provideAnswerReportRequestWithEmptyDescription() { + return Stream.of(신고_내용이_null인_답변_신고_request, 신고_내용이_빈값인_답변_신고_request); + } + + @Test + void 전체_답변_신고_목록을_조회한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(answerReportService.readAll()).willReturn(List.of(답변_신고_dto1, 답변_신고_dto2, 답변_신고_dto3)); + + // when & then + final ResultActions resultActions = + mockMvc.perform(get("/reports/answers") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpectAll( + status().isOk(), + jsonPath("$.reports.[0].id", is(답변_신고_dto1.id()), Long.class), + jsonPath("$.reports.[0].reporter.id", is(답변_신고_dto1.reporterDto().id()), Long.class), + jsonPath("$.reports.[0].reporter.name", is(답변_신고_dto1.reporterDto().name())), + jsonPath("$.reports.[0].createdTime").exists(), + jsonPath("$.reports.[0].answer.id", is(답변_신고_dto1.answerDto().id()), Long.class), + jsonPath("$.reports.[0].description", is(답변_신고_dto1.description())), + jsonPath("$.reports.[1].id", is(답변_신고_dto2.id()), Long.class), + jsonPath("$.reports.[1].reporter.id", is(답변_신고_dto2.reporterDto().id()), Long.class), + jsonPath("$.reports.[1].reporter.name", is(답변_신고_dto2.reporterDto().name())), + jsonPath("$.reports.[1].createdTime").exists(), + jsonPath("$.reports.[1].answer.id", is(답변_신고_dto2.answerDto().id()), Long.class), + jsonPath("$.reports.[1].description", is(답변_신고_dto2.description())), + jsonPath("$.reports.[2].id", is(답변_신고_dto3.id()), Long.class), + jsonPath("$.reports.[2].reporter.id", is(답변_신고_dto3.reporterDto().id()), Long.class), + jsonPath("$.reports.[2].reporter.name", is(답변_신고_dto3.reporterDto().name())), + jsonPath("$.reports.[2].createdTime").exists(), + jsonPath("$.reports.[2].answer.id", is(답변_신고_dto3.answerDto().id()), Long.class), + jsonPath("$.reports.[2].description", is(답변_신고_dto3.description())) + ); + + readAllAnswerReport_문서화(resultActions); + } + + private void createAuctionReport_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + requestFields( + fieldWithPath("auctionId").description("신고할 경매 ID"), + fieldWithPath("description").description("신고 내용") + ) + ) + ); + } + + private void readAllAuctionReport_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + responseFields( + fieldWithPath("reports.[]").type(JsonFieldType.ARRAY) + .description("모든 경매 신고 목록"), + fieldWithPath("reports.[].id").type(JsonFieldType.NUMBER) + .description("경매 신고 글 ID"), + fieldWithPath("reports.[].reporter.id").type(JsonFieldType.NUMBER) + .description("경매 신고한 사용자의 ID"), + fieldWithPath("reports.[].reporter.name").type(JsonFieldType.STRING) + .description("경매 신고한 사용자의 이름"), + fieldWithPath("reports.[].createdTime").type(JsonFieldType.STRING) + .description("경매 신고 시간"), + fieldWithPath("reports.[].auction.id").type(JsonFieldType.NUMBER) + .description("신고한 경매 ID"), + fieldWithPath("reports.[].auction.title").type(JsonFieldType.STRING) + .description("신고한 경매 제목"), + fieldWithPath("reports.[].description").type(JsonFieldType.STRING) + .description("신고 내용") + ) + ) ); - final ReadUserInReportDto buyerDto1 = new ReadUserInReportDto(2L, "구매자1", "profile.png", 4.0d, "12346"); - final ReadChatRoomReportDto chatRoomReportDto1 = new ReadChatRoomReportDto( - 1L, - new ReadReporterDto(1L, "회원1", "이미지1", 5.0), - LocalDateTime.now(), - new ReadChatRoomInReportDto(1L, auctionInReportDto, buyerDto1, false), - "신고합니다." + } + + private void createChatRoomReport_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + requestFields( + fieldWithPath("chatRoomId").description("신고할 채팅방 ID"), + fieldWithPath("description").description("신고 내용") + ) + ) ); - final ReadUserInReportDto buyerDto2 = new ReadUserInReportDto(3L, "구매자2", "profile.png", 4.0d, "12347"); - final ReadChatRoomReportDto chatRoomReportDto2 = new ReadChatRoomReportDto( - 2L, - new ReadReporterDto(1L, "회원1", "이미지1", 5.0), - LocalDateTime.now(), - new ReadChatRoomInReportDto(1L, auctionInReportDto, buyerDto2, false), - "신고합니다." + } + + private void readAllChatRoomReport_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + responseFields( + fieldWithPath("reports.[]").type(JsonFieldType.ARRAY) + .description("모든 채팅방 신고 목록"), + fieldWithPath("reports.[].id").type(JsonFieldType.NUMBER) + .description("채팅방 신고 글 ID"), + fieldWithPath("reports.[].reporter.id").type(JsonFieldType.NUMBER) + .description("채팅방을 신고한 사용자의 ID"), + fieldWithPath("reports.[].reporter.name").type(JsonFieldType.STRING) + .description("채팅방을 신고한 사용자의 이름"), + fieldWithPath("reports.[].createdTime").type(JsonFieldType.STRING) + .description("채팅방 신고 시간"), + fieldWithPath("reports.[].chatRoom.id").type(JsonFieldType.NUMBER) + .description("신고한 채팅방 ID"), + fieldWithPath("reports.[].description").type(JsonFieldType.STRING) + .description("신고 내용") + ) + ) ); - final ReadUserInReportDto buyerDto3 = new ReadUserInReportDto(4L, "구매자3", "profile.png", 4.0d, "12348"); - final ReadChatRoomReportDto chatRoomReportDto3 = new ReadChatRoomReportDto( - 3L, - new ReadReporterDto(1L, "회원1", "이미지1", 5.0), - LocalDateTime.now(), - new ReadChatRoomInReportDto(1L, auctionInReportDto, buyerDto3, false), - "신고합니다." + } + + private void createQuestionReport_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + requestFields( + fieldWithPath("auctionId").description("질문의 경매 ID"), + fieldWithPath("questionId").description("신고할 질문 ID"), + fieldWithPath("description").description("신고 내용") + ) + ) + ); + } + + private void readAllQuestionReport_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + responseFields( + fieldWithPath("reports.[]").type(JsonFieldType.ARRAY) + .description("모든 질문 신고 목록"), + fieldWithPath("reports.[].id").type(JsonFieldType.NUMBER) + .description("질문 신고 글 ID"), + fieldWithPath("reports.[].reporter.id").type(JsonFieldType.NUMBER) + .description("질문을 신고한 사용자의 ID"), + fieldWithPath("reports.[].reporter.name").type(JsonFieldType.STRING) + .description("질문을 신고한 사용자의 이름"), + fieldWithPath("reports.[].createdTime").type(JsonFieldType.STRING) + .description("질문 신고 시간"), + fieldWithPath("reports.[].question.id").type(JsonFieldType.NUMBER) + .description("신고한 질문 ID"), + fieldWithPath("reports.[].description").type(JsonFieldType.STRING) + .description("신고 내용") + ) + ) + ); + } + + private void createAnswerReport_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + requestFields( + fieldWithPath("auctionId").description("질문의 경매 ID"), + fieldWithPath("answerId").description("신고할 답변 ID"), + fieldWithPath("description").description("신고 내용") + ) + ) + ); + } + + private void readAllAnswerReport_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + responseFields( + fieldWithPath("reports.[]").type(JsonFieldType.ARRAY) + .description("모든 답변 신고 목록"), + fieldWithPath("reports.[].id").type(JsonFieldType.NUMBER) + .description("답변 신고 글 ID"), + fieldWithPath("reports.[].reporter.id").type(JsonFieldType.NUMBER) + .description("답변을 신고한 사용자의 ID"), + fieldWithPath("reports.[].reporter.name").type(JsonFieldType.STRING) + .description("답변을 신고한 사용자의 이름"), + fieldWithPath("reports.[].createdTime").type(JsonFieldType.STRING) + .description("답변 신고 시간"), + fieldWithPath("reports.[].answer.id").type(JsonFieldType.NUMBER) + .description("신고한 답변 ID"), + fieldWithPath("reports.[].description").type(JsonFieldType.STRING) + .description("신고 내용") + ) + ) ); - given(chatRoomReportService.readAll()) - .willReturn(List.of(chatRoomReportDto1, chatRoomReportDto2, chatRoomReportDto3)); - - // when & then - mockMvc.perform(get("/reports/chat-rooms") - .contentType(MediaType.APPLICATION_JSON) - ) - .andExpectAll( - status().isOk(), - jsonPath("$.reports.[0].id", is(chatRoomReportDto1.id()), Long.class), - jsonPath("$.reports.[0].reporter.id", is(chatRoomReportDto1.reporterDto().id()), Long.class), - jsonPath("$.reports.[0].reporter.name", is(chatRoomReportDto1.reporterDto().name())), - jsonPath("$.reports.[0].createdTime").exists(), - jsonPath("$.reports.[0].chatRoom.id", is(chatRoomReportDto1.chatRoomDto().id()), Long.class), - jsonPath("$.reports.[0].description", is(chatRoomReportDto1.description())), - jsonPath("$.reports.[1].id", is(chatRoomReportDto2.id()), Long.class), - jsonPath("$.reports.[1].reporter.id", is(chatRoomReportDto2.reporterDto().id()), Long.class), - jsonPath("$.reports.[1].reporter.name", is(chatRoomReportDto2.reporterDto().name())), - jsonPath("$.reports.[1].createdTime").exists(), - jsonPath("$.reports.[1].chatRoom.id", is(chatRoomReportDto2.chatRoomDto().id()), Long.class), - jsonPath("$.reports.[1].description", is(chatRoomReportDto2.description())), - jsonPath("$.reports.[2].id", is(chatRoomReportDto3.id()), Long.class), - jsonPath("$.reports.[2].reporter.id", is(chatRoomReportDto3.reporterDto().id()), Long.class), - jsonPath("$.reports.[2].reporter.name", is(chatRoomReportDto3.reporterDto().name())), - jsonPath("$.reports.[2].createdTime").exists(), - jsonPath("$.reports.[2].chatRoom.id", is(chatRoomReportDto3.chatRoomDto().id()), Long.class), - jsonPath("$.reports.[2].description", is(chatRoomReportDto3.description())) - ) - .andDo( - restDocs.document( - responseFields( - fieldWithPath("reports.[]").type(JsonFieldType.ARRAY).description("모든 채팅방 신고 목록"), - fieldWithPath("reports.[].id").type(JsonFieldType.NUMBER).description("채팅방 신고 글 ID"), - fieldWithPath("reports.[].reporter.id").type(JsonFieldType.NUMBER).description("채팅방을 신고한 사용자의 ID"), - fieldWithPath("reports.[].reporter.name").type(JsonFieldType.STRING).description("채팅방을 신고한 사용자의 이름"), - fieldWithPath("reports.[].createdTime").type(JsonFieldType.STRING).description("채팅방 신고 시간"), - fieldWithPath("reports.[].chatRoom.id").type(JsonFieldType.NUMBER).description("신고한 채팅방 ID"), - fieldWithPath("reports.[].description").type(JsonFieldType.STRING).description("신고 내용") - ) - ) - ); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/report/presentation/fixture/ReportControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/report/presentation/fixture/ReportControllerFixture.java new file mode 100644 index 000000000..ae67ccf80 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/report/presentation/fixture/ReportControllerFixture.java @@ -0,0 +1,176 @@ +package com.ddang.ddang.report.presentation.fixture; + +import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims; +import com.ddang.ddang.configuration.CommonControllerSliceTest; +import com.ddang.ddang.report.application.dto.ReadAnswerInReportDto; +import com.ddang.ddang.report.application.dto.ReadAnswerReportDto; +import com.ddang.ddang.report.application.dto.ReadAuctionInReportDto; +import com.ddang.ddang.report.application.dto.ReadAuctionReportDto; +import com.ddang.ddang.report.application.dto.ReadChatRoomInReportDto; +import com.ddang.ddang.report.application.dto.ReadChatRoomReportDto; +import com.ddang.ddang.report.application.dto.ReadQuestionInReportDto; +import com.ddang.ddang.report.application.dto.ReadQuestionReportDto; +import com.ddang.ddang.report.application.dto.ReadReporterDto; +import com.ddang.ddang.report.application.dto.ReadUserInReportDto; +import com.ddang.ddang.report.presentation.dto.request.CreateAnswerReportRequest; +import com.ddang.ddang.report.presentation.dto.request.CreateAuctionReportRequest; +import com.ddang.ddang.report.presentation.dto.request.CreateChatRoomReportRequest; +import com.ddang.ddang.report.presentation.dto.request.CreateQuestionReportRequest; + +import java.time.LocalDateTime; + +@SuppressWarnings("NonAsciiCharacters") +public class ReportControllerFixture extends CommonControllerSliceTest { + + protected Long 생성된_경매_신고_아이디 = 1L; + protected Long 생성된_채팅방_신고_아이디 = 1L; + protected Long 생성된_질문_신고_아이디 = 1L; + protected Long 생성된_답변_신고_아이디 = 1L; + protected String 엑세스_토큰_값 = "Bearer accessToken"; + protected PrivateClaims 사용자_ID_클레임 = new PrivateClaims(1L); + protected PrivateClaims 존재하지_않는_사용자_ID_클레임 = new PrivateClaims(-999L); + protected PrivateClaims 채팅방_참여자가_아닌_사용자_ID_클레임 = new PrivateClaims(999L); + protected CreateAuctionReportRequest 경매_신고_request = new CreateAuctionReportRequest(1L, "신고합니다"); + protected CreateAuctionReportRequest 경매_아이디가_없는_신고_request = new CreateAuctionReportRequest(null, "신고합니다"); + protected CreateAuctionReportRequest 경매_아이디가_음수인_신고_request = new CreateAuctionReportRequest(-999L, "신고합니다"); + protected static CreateAuctionReportRequest 신고_내용이_null인_경매_신고_request = new CreateAuctionReportRequest(1L, null); + protected static CreateAuctionReportRequest 신고_내용이_빈값인_경매_신고_request = new CreateAuctionReportRequest(1L, ""); + private ReadUserInReportDto 판매자_정보_dto = new ReadUserInReportDto(1L, "판매자", 1L, 4.0d, "12345", false); + private ReadAuctionInReportDto 신고할_경매_정보_dto = new ReadAuctionInReportDto( + 1L, + 판매자_정보_dto, + "제목", + "설명", + 100, + 1_00, + false, + LocalDateTime.now().plusDays(2), + 2 + ); + protected ReadAuctionReportDto 경매_신고_dto1 = new ReadAuctionReportDto( + 1L, + new ReadReporterDto(2L, "회원1", 2L, 5.0, false), + LocalDateTime.now(), + 신고할_경매_정보_dto, + "신고합니다." + ); + protected ReadAuctionReportDto 경매_신고_dto2 = new ReadAuctionReportDto( + 2L, + new ReadReporterDto(3L, "회원2", 3L, 5.0, false), + LocalDateTime.now(), + 신고할_경매_정보_dto, + "신고합니다." + ); + protected ReadAuctionReportDto 경매_신고_dto3 = new ReadAuctionReportDto( + 3L, + new ReadReporterDto(4L, "회원3", 4L, 5.0, false), + LocalDateTime.now(), + 신고할_경매_정보_dto, + "신고합니다." + ); + + protected CreateChatRoomReportRequest 채팅방_신고_request = new CreateChatRoomReportRequest(1L, "신고합니다"); + protected CreateChatRoomReportRequest 존재하지_않는_채팅방_신고_request = new CreateChatRoomReportRequest(9999L, "신고합니다"); + protected CreateChatRoomReportRequest 채팅방_아이디가_null인_신고_request = new CreateChatRoomReportRequest(null, "신고합니다"); + protected CreateChatRoomReportRequest 채팅방_아이디가_음수인_신고_request = new CreateChatRoomReportRequest(-999L, "신고합니다"); + protected static CreateChatRoomReportRequest 신고_내용이_null인_채팅_신고_request = new CreateChatRoomReportRequest(1L, null); + protected static CreateChatRoomReportRequest 신고_내용이_빈값인_채팅_신고_request = new CreateChatRoomReportRequest(-999L, ""); + private ReadAuctionInReportDto 신고할_채팅방의_경매_정보_dto1 = new ReadAuctionInReportDto( + 1L, + 판매자_정보_dto, + "제목", + "설명", + 100, + 1_00, + false, + LocalDateTime.now().plusDays(2), + 2 + ); + private ReadUserInReportDto 구매자_정보_dto1 = new ReadUserInReportDto(2L, "구매자1", 2L, 4.0d, "12346", false); + private ReadReporterDto 신고자_정보_dto1 = new ReadReporterDto(2L, "구매자1", 2L, 4.0d, false); + protected ReadChatRoomReportDto 채팅방_신고_dto1 = new ReadChatRoomReportDto( + 1L, + 신고자_정보_dto1, + LocalDateTime.now(), + new ReadChatRoomInReportDto(1L, 신고할_채팅방의_경매_정보_dto1, 구매자_정보_dto1), + "신고합니다." + ); + private ReadAuctionInReportDto 신고할_채팅방의_경매_정보_dto2 = new ReadAuctionInReportDto( + 2L, + 판매자_정보_dto, + "제목", + "설명", + 100, + 1_00, + false, + LocalDateTime.now().plusDays(2), + 2 + ); + private ReadUserInReportDto 구매자_정보_dto2 = new ReadUserInReportDto(3L, "구매자2", 3L, 4.0d, "12347", false); + private ReadReporterDto 신고자_정보_dto2 = new ReadReporterDto(3L, "구매자2", 3L, 4.0d, false); + protected ReadChatRoomReportDto 채팅방_신고_dto2 = new ReadChatRoomReportDto( + 2L, + 신고자_정보_dto2, + LocalDateTime.now(), + new ReadChatRoomInReportDto(1L, 신고할_채팅방의_경매_정보_dto2, 구매자_정보_dto2), + "신고합니다." + ); + private ReadAuctionInReportDto 신고할_채팅방의_경매_정보_dto3 = new ReadAuctionInReportDto( + 3L, + 판매자_정보_dto, + "제목", + "설명", + 100, + 1_00, + false, + LocalDateTime.now().plusDays(2), + 2 + ); + private ReadUserInReportDto 구매자_정보_dto3 = new ReadUserInReportDto(3L, "구매자2", 3L, 4.0d, "12347", false); + private ReadReporterDto 신고자_정보_dto3 = new ReadReporterDto(3L, "구매자2", 3L, 4.0d, false); + protected ReadChatRoomReportDto 채팅방_신고_dto3 = new ReadChatRoomReportDto( + 3L, + 신고자_정보_dto3, + LocalDateTime.now(), + new ReadChatRoomInReportDto(1L, 신고할_채팅방의_경매_정보_dto3, 구매자_정보_dto3), + "신고합니다." + ); + + protected CreateQuestionReportRequest 질문_신고_request = new CreateQuestionReportRequest(1L, 1L, "신고합니다."); + protected CreateQuestionReportRequest 존재하지_않는_질문_신고_request = new CreateQuestionReportRequest(1L, 999L, "신고합니다."); + protected CreateQuestionReportRequest 본인의_질문_신고_request = new CreateQuestionReportRequest(1L, 1L, "신고합니다."); + protected CreateQuestionReportRequest 이미_신고한_질문_신고_request = new CreateQuestionReportRequest(1L, 1L, "신고합니다."); + protected CreateQuestionReportRequest 경매_아이디가_null인_질문_신고_request = new CreateQuestionReportRequest(null, 1L, "신고합니다."); + protected CreateQuestionReportRequest 경매_아이디가_음수인_질문_신고_request = new CreateQuestionReportRequest(-1L, 1L, "신고합니다."); + protected CreateQuestionReportRequest 질문_아이디가_null인_질문_신고_request = new CreateQuestionReportRequest(1L, null, "신고합니다."); + protected CreateQuestionReportRequest 질문_아이디가_음수인_질문_신고_request = new CreateQuestionReportRequest(1L, -1L, "신고합니다."); + protected static CreateQuestionReportRequest 신고_내용이_null인_질문_신고_request = new CreateQuestionReportRequest(1L, 1L, null); + protected static CreateQuestionReportRequest 신고_내용이_빈값인_질문_신고_request = new CreateQuestionReportRequest(1L, 1L, ""); + + private ReadUserInReportDto 질문자_dto = new ReadUserInReportDto(1L, "사용자", 1L, 5.0d, "12345", false); + private ReadQuestionInReportDto 질문_dto1 = new ReadQuestionInReportDto(1L, 질문자_dto, "질문드립니다.", LocalDateTime.now()); + private ReadQuestionInReportDto 질문_dto2 = new ReadQuestionInReportDto(2L, 질문자_dto, "질문드립니다.", LocalDateTime.now()); + private ReadQuestionInReportDto 질문_dto3 = new ReadQuestionInReportDto(3L, 질문자_dto, "질문드립니다.", LocalDateTime.now()); + protected ReadQuestionReportDto 질문_신고_dto1 = new ReadQuestionReportDto(1L, 신고자_정보_dto1, LocalDateTime.now(), 질문_dto1, "신고합니다."); + protected ReadQuestionReportDto 질문_신고_dto2 = new ReadQuestionReportDto(2L, 신고자_정보_dto1, LocalDateTime.now(), 질문_dto2, "신고합니다."); + protected ReadQuestionReportDto 질문_신고_dto3 = new ReadQuestionReportDto(3L, 신고자_정보_dto1, LocalDateTime.now(), 질문_dto3, "신고합니다."); + + protected CreateAnswerReportRequest 답변_신고_request = new CreateAnswerReportRequest(1L, 1L, "신고합니다."); + protected CreateAnswerReportRequest 존재하지_않는_답변_신고_request = new CreateAnswerReportRequest(1L, 999L, "신고합니다."); + protected CreateAnswerReportRequest 본인의_답변_신고_request = new CreateAnswerReportRequest(1L, 1L, "신고합니다."); + protected CreateAnswerReportRequest 이미_신고한_답변_신고_request = new CreateAnswerReportRequest(1L, 1L, "신고합니다."); + protected CreateAnswerReportRequest 경매_아이디가_null인_답변_신고_request = new CreateAnswerReportRequest(null, 1L, "신고합니다."); + protected CreateAnswerReportRequest 경매_아이디가_음수인_답변_신고_request = new CreateAnswerReportRequest(-1L, 1L, "신고합니다."); + protected CreateAnswerReportRequest 질문_아이디가_null인_답변_신고_request = new CreateAnswerReportRequest(1L, null, "신고합니다."); + protected CreateAnswerReportRequest 답변_아이디가_음수인_질문_신고_request = new CreateAnswerReportRequest(1L, -1L, "신고합니다."); + protected static CreateAnswerReportRequest 신고_내용이_null인_답변_신고_request = new CreateAnswerReportRequest(1L, 1L, null); + protected static CreateAnswerReportRequest 신고_내용이_빈값인_답변_신고_request = new CreateAnswerReportRequest(1L, 1L, ""); + private ReadUserInReportDto 답변자_dto = new ReadUserInReportDto(1L, "사용자", 1L, 5.0d, "12345", false); + private ReadAnswerInReportDto 답변_dto1 = new ReadAnswerInReportDto(1L, 답변자_dto, "답변드립니다.", LocalDateTime.now()); + private ReadAnswerInReportDto 답변_dto2 = new ReadAnswerInReportDto(2L, 답변자_dto, "답변드립니다.", LocalDateTime.now()); + private ReadAnswerInReportDto 답변_dto3 = new ReadAnswerInReportDto(3L, 답변자_dto, "답변드립니다.", LocalDateTime.now()); + protected ReadAnswerReportDto 답변_신고_dto1 = new ReadAnswerReportDto(1L, 신고자_정보_dto1, LocalDateTime.now(), 답변_dto1, "신고합니다."); + protected ReadAnswerReportDto 답변_신고_dto2 = new ReadAnswerReportDto(2L, 신고자_정보_dto1, LocalDateTime.now(), 답변_dto2, "신고합니다."); + protected ReadAnswerReportDto 답변_신고_dto3 = new ReadAnswerReportDto(3L, 신고자_정보_dto1, LocalDateTime.now(), 답변_dto3, "신고합니다."); + +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/review/application/ReviewServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/review/application/ReviewServiceTest.java new file mode 100644 index 000000000..65307b708 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/review/application/ReviewServiceTest.java @@ -0,0 +1,144 @@ +package com.ddang.ddang.review.application; + +import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; +import com.ddang.ddang.configuration.IsolateDatabase; +import com.ddang.ddang.review.application.dto.ReadReviewDetailDto; +import com.ddang.ddang.review.application.dto.ReadReviewDto; +import com.ddang.ddang.review.application.exception.AlreadyReviewException; +import com.ddang.ddang.review.application.exception.InvalidUserToReview; +import com.ddang.ddang.review.application.exception.ReviewNotFoundException; +import com.ddang.ddang.review.application.fixture.ReviewServiceFixture; +import com.ddang.ddang.user.application.exception.UserNotFoundException; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +@IsolateDatabase +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class ReviewServiceTest extends ReviewServiceFixture { + + @Autowired + ReviewService reviewService; + + @Test + void 평가를_등록한고_평가_상대의_신뢰도를_갱신한다() { + // when + final Long actual = reviewService.create(구매자에_대한_평가_등록을_위한_DTO); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isPositive(); + softAssertions.assertThat(구매자.getReliability().getValue()).isEqualTo(구매자가_새로운_평가_점수를_받고난_후의_신뢰도_점수); + }); + } + + @Test + void 연관된_경매를_찾을_수_없다면_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> reviewService.create(존재하지_않는_경매와_연관된_평가를_등록하려는_DTO)) + .isInstanceOf(AuctionNotFoundException.class) + .hasMessage("해당 경매를 찾을 수 없습니다."); + } + + @Test + void 평가_작성자를_찾을_수_없다면_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> reviewService.create(존재하지_않는_사용자가_평가를_등록하려는_DTO)) + .isInstanceOf(UserNotFoundException.class) + .hasMessage("작성자 정보를 찾을 수 없습니다."); + } + + @Test + void 평가_상대를_찾을_수_없다면_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> reviewService.create(존재하지_않는_사용자를_평가하려는_DTO)) + .isInstanceOf(UserNotFoundException.class) + .hasMessage("평가 상대의 정보를 찾을 수 없습니다."); + } + + @Test + void 경매의_판매자나_낙찰자가_아닌_사용자일_경우_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> reviewService.create(경매_참여자가_아닌_사용자가_평가를_등록하려는_DTO)) + .isInstanceOf(InvalidUserToReview.class) + .hasMessage("경매의 판매자 또는 최종 낙찰자만 평가가 가능합니다."); + } + + @Test + void 이미_평가했는데_평가를_등록한다면_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> reviewService.create(이미_평가한_경매와_연관된_평가를_또_등록하려는_DTO)) + .isInstanceOf(AlreadyReviewException.class) + .hasMessage("이미 평가하였습니다."); + } + + @Test + void 지정한_평가_아이디에_해당하는_평가를_조회한다() { + // when + final ReadReviewDetailDto actual = reviewService.readByReviewId(구매자가_판매자1에게_받은_평가.getId()); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual.score()).isEqualTo(구매자가_판매자1에게_받은_평가.getScore().getValue()); + softAssertions.assertThat(actual.content()).isEqualTo(구매자가_판매자1에게_받은_평가.getContent()); + }); + } + + @Test + void 지정한_평가_아이디에_해당하는_평가가_없으면_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> reviewService.readByReviewId(존재하지_않는_평가_아이디)) + .isInstanceOf(ReviewNotFoundException.class) + .hasMessage("해당 평가를 찾을 수 없습니다."); + } + + @Test + void 지정한_아이디가_평가_대상인_평가_목록을_최신순으로_조회한다() { + // when + final List actual = reviewService.readAllByTargetId(구매자.getId()); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(2); + softAssertions.assertThat(actual.get(0).id()).isEqualTo(구매자가_판매자2에게_받은_평가.getId()); + softAssertions.assertThat(actual.get(0).writer().id()).isEqualTo(판매자2.getId()); + softAssertions.assertThat(actual.get(0).content()).isEqualTo(구매자가_판매자2에게_받은_평가.getContent()); + softAssertions.assertThat(actual.get(0).score()).isEqualTo(구매자가_판매자2에게_받은_평가.getScore().getValue()); + softAssertions.assertThat(actual.get(1).id()).isEqualTo(구매자가_판매자1에게_받은_평가.getId()); + softAssertions.assertThat(actual.get(1).writer().id()).isEqualTo(판매자1.getId()); + softAssertions.assertThat(actual.get(1).content()).isEqualTo(구매자가_판매자1에게_받은_평가.getContent()); + softAssertions.assertThat(actual.get(1).score()).isEqualTo(구매자가_판매자1에게_받은_평가.getScore().getValue()); + }); + } + + @Test + void 지정한_경매_아이디와_작성자_아이디가_해당하는_평가가_존재한다면_dto에_넣어_반환한다() { + // when + final ReadReviewDetailDto actual = reviewService.readByAuctionIdAndWriterId(판매자1.getId(), 판매자1이_평가한_경매.getId()); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual.score()).isEqualTo(구매자가_판매자1에게_받은_평가.getScore().getValue()); + softAssertions.assertThat(actual.content()).isEqualTo(구매자가_판매자1에게_받은_평가.getContent()); + }); + } + + @Test + void 지정한_경매_아이디와_작성자_아이디가_해당하는_평가가_존재하지_않는다면_dto의_필드가_null이다() { + // when + final ReadReviewDetailDto actual = reviewService.readByAuctionIdAndWriterId(평가_안한_경매_판매자.getId(), 평가_안한_경매.getId()); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual.score()).isNull(); + softAssertions.assertThat(actual.content()).isNull(); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/review/application/fixture/ReviewServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/review/application/fixture/ReviewServiceFixture.java new file mode 100644 index 000000000..510d1ea42 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/review/application/fixture/ReviewServiceFixture.java @@ -0,0 +1,180 @@ +package com.ddang.ddang.review.application.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.review.application.dto.CreateReviewDto; +import com.ddang.ddang.review.domain.Review; +import com.ddang.ddang.review.domain.Score; +import com.ddang.ddang.review.infrastructure.persistence.JpaReviewRepository; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class ReviewServiceFixture { + + @Autowired + private JpaCategoryRepository categoryRepository; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaReviewRepository reviewRepository; + + private Double 구매자가_판매자1에게_받은_평가_점수 = 5.0d; + private Double 구매자가_판매자2에게_받은_평가_점수 = 1.0d; + private Double 구매자가_받을_새로운_평가_점수 = 4.5d; + + protected Double 구매자가_새로운_평가_점수를_받고난_후의_신뢰도_점수 = + (구매자가_판매자1에게_받은_평가_점수 + 구매자가_판매자2에게_받은_평가_점수 + 구매자가_받을_새로운_평가_점수) / 3; + protected Long 존재하지_않는_사용자 = -999L; + protected User 판매자1; + protected User 판매자2; + protected User 평가_안한_경매_판매자; + protected User 구매자; + protected User 경매_참여자가_아닌_사용자; + protected Long 존재하지_않는_경매_아이디 = -999L; + protected Auction 판매자1이_평가한_경매; + protected Auction 판매자2가_평가한_경매; + protected Auction 평가_안한_경매; + protected Long 존재하지_않는_평가_아이디 = -999L; + protected Review 구매자가_판매자1에게_받은_평가; + protected Review 구매자가_판매자2에게_받은_평가; + protected List 구매자가_이전까지_받은_평가_총2개; + protected CreateReviewDto 구매자에_대한_평가_등록을_위한_DTO; + protected CreateReviewDto 존재하지_않는_경매와_연관된_평가를_등록하려는_DTO; + protected CreateReviewDto 존재하지_않는_사용자가_평가를_등록하려는_DTO; + protected CreateReviewDto 존재하지_않는_사용자를_평가하려는_DTO; + protected CreateReviewDto 경매_참여자가_아닌_사용자가_평가를_등록하려는_DTO; + protected CreateReviewDto 이미_평가한_경매와_연관된_평가를_또_등록하려는_DTO; + + @BeforeEach + void setUp() { + final Category 전자기기_카테고리 = new Category("전자기기"); + final Category 전자기기_서브_노트북_카테고리 = new Category("노트북 카테고리"); + final ProfileImage 판매자1_프로필_이미지 = new ProfileImage("seller1_profile.png", "seller1_profile.png"); + final ProfileImage 판매자2_프로필_이미지 = new ProfileImage("seller2_profile.png", "seller2_profile.png"); + final ProfileImage 평가_안한_판매자_프로필_이미지 = new ProfileImage("no_review_seller_profile.png", "no_review_seller_profile.png"); + final ProfileImage 구매자_프로필_이미지 = new ProfileImage("buyer_profile.png", "buyer_profile.png"); + final AuctionImage 경매1_대표_이미지 = new AuctionImage("경매1_대표_이미지.png", "경매1_대표_이미지.png"); + final AuctionImage 경매2_대표_이미지 = new AuctionImage("경매2_대표_이미지.png", "경매2_대표_이미지.png"); + final AuctionImage 평가_안한_경매_대표_이미지 = new AuctionImage("평가_안한_경매_대표_이미지.png", "평가_안한_경매_대표_이미지.png"); + + 전자기기_카테고리.addSubCategory(전자기기_서브_노트북_카테고리); + categoryRepository.save(전자기기_카테고리); + + 판매자1 = User.builder() + .name("판매자1") + .profileImage(판매자1_프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + 판매자2 = User.builder() + .name("판매자2") + .profileImage(판매자2_프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + 평가_안한_경매_판매자 = User.builder() + .name("평가 안한 판매자") + .profileImage(평가_안한_판매자_프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + 구매자 = User.builder() + .name("구매자") + .profileImage(구매자_프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + 경매_참여자가_아닌_사용자 = User.builder() + .name("경매 참여자가 아닌 사용자") + .profileImage(new ProfileImage("profile.png", "profile.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + userRepository.saveAll(List.of(판매자1, 판매자2, 평가_안한_경매_판매자, 구매자, 경매_참여자가_아닌_사용자)); + + 판매자1이_평가한_경매 = Auction.builder() + .seller(판매자1) + .title("맥북") + .description("맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 판매자2가_평가한_경매 = Auction.builder() + .seller(판매자2) + .title("맥북") + .description("맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 평가_안한_경매 = Auction.builder() + .seller(평가_안한_경매_판매자) + .title("맥북") + .description("맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 판매자1이_평가한_경매.addAuctionImages(List.of(경매1_대표_이미지)); + 판매자2가_평가한_경매.addAuctionImages(List.of(경매2_대표_이미지)); + 평가_안한_경매.addAuctionImages(List.of(평가_안한_경매_대표_이미지)); + auctionRepository.saveAll(List.of(판매자1이_평가한_경매, 판매자2가_평가한_경매, 평가_안한_경매)); + + 구매자가_판매자1에게_받은_평가 = Review.builder() + .auction(판매자1이_평가한_경매) + .writer(판매자1) + .target(구매자) + .content("친절하다.") + .score(new Score(구매자가_판매자1에게_받은_평가_점수)) + .build(); + 구매자가_판매자2에게_받은_평가 = Review.builder() + .auction(판매자2가_평가한_경매) + .writer(판매자2) + .target(구매자) + .content("별로다.") + .score(new Score(구매자가_판매자2에게_받은_평가_점수)) + .build(); + reviewRepository.saveAll(List.of(구매자가_판매자1에게_받은_평가, 구매자가_판매자2에게_받은_평가)); + + 구매자가_이전까지_받은_평가_총2개 = List.of(구매자가_판매자1에게_받은_평가, 구매자가_판매자2에게_받은_평가); + + 구매자에_대한_평가_등록을_위한_DTO = new CreateReviewDto( + 평가_안한_경매.getId(), + 평가_안한_경매_판매자.getId(), + 구매자.getId(), "친절하다.", + 구매자가_받을_새로운_평가_점수 + ); + 존재하지_않는_경매와_연관된_평가를_등록하려는_DTO = + new CreateReviewDto(존재하지_않는_경매_아이디, 구매자.getId(), 판매자1.getId(), "친절하다.", 5.0d); + 존재하지_않는_사용자가_평가를_등록하려는_DTO = + new CreateReviewDto(평가_안한_경매.getId(), 존재하지_않는_사용자, 평가_안한_경매_판매자.getId(), "친절하다.", 5.0d); + 존재하지_않는_사용자를_평가하려는_DTO = + new CreateReviewDto(평가_안한_경매.getId(), 구매자.getId(), 존재하지_않는_사용자, "친절하다.", 5.0d); + 경매_참여자가_아닌_사용자가_평가를_등록하려는_DTO = + new CreateReviewDto(평가_안한_경매.getId(), 경매_참여자가_아닌_사용자.getId(), 구매자.getId(), "친절하다", 5.0d); + 이미_평가한_경매와_연관된_평가를_또_등록하려는_DTO = + new CreateReviewDto(판매자1이_평가한_경매.getId(), 판매자1.getId(), 구매자.getId(), "친절하다", 5.0d); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/review/domain/ScoreTest.java b/backend/ddang/src/test/java/com/ddang/ddang/review/domain/ScoreTest.java new file mode 100644 index 000000000..7bdb15da5 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/review/domain/ScoreTest.java @@ -0,0 +1,31 @@ +package com.ddang.ddang.review.domain; + +import com.ddang.ddang.review.domain.exception.InvalidScoreException; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class ScoreTest { + + @ParameterizedTest(name = "평가 점수가 {0}점일 때") + @ValueSource(doubles = {0.5d, 1.0d}) + void 평가_점수가_0점5_단위라면_정상적으로_생성된다(double scoreValue) { + // when & then + assertThatCode(() -> new Score(scoreValue)).doesNotThrowAnyException(); + } + + @ParameterizedTest(name = "평가 점수가 {0}점일 때") + @ValueSource(doubles = {0.1d, 0.9d}) + void 평가_점수가_0점5_단위가_아니라면_예외가_발생한다(double invalidScoreValue) { + // when & then + assertThatThrownBy(() -> new Score(invalidScoreValue)) + .isInstanceOf(InvalidScoreException.class) + .hasMessage("평가 점수는 0.5 단위여야 합니다."); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/review/infrastructure/persistence/JpaReviewRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/review/infrastructure/persistence/JpaReviewRepositoryTest.java new file mode 100644 index 000000000..1a491874c --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/review/infrastructure/persistence/JpaReviewRepositoryTest.java @@ -0,0 +1,96 @@ +package com.ddang.ddang.review.infrastructure.persistence; + +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.ddang.ddang.review.domain.Review; +import com.ddang.ddang.review.infrastructure.persistence.fixture.JpaReviewRepositoryFixture; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class JpaReviewRepositoryTest extends JpaReviewRepositoryFixture { + + @PersistenceContext + EntityManager em; + + @Autowired + JpaReviewRepository reviewRepository; + + @Test + void 평가를_저장한다() { + // when + reviewRepository.save(저장하려는_평가); + + // then + em.flush(); + em.clear(); + + assertThat(저장하려는_평가.getId()).isPositive(); + } + + @Test + void 지정한_경매_아이디와_작성자_아이디를_포함하는_평가가_존재하면_참을_반환한다() { + // when + final boolean actual = reviewRepository.existsByAuctionIdAndWriterId(판매자1이_평가한_경매.getId(), 판매자1.getId()); + + // then + assertThat(actual).isTrue(); + } + + @Test + void 지정한_채팅방_아이디를_포함하는_평가가_존재하지_않는다면_거짓을_반환한다() { + // when + final boolean actual = reviewRepository.existsByAuctionIdAndWriterId(평가_안한_경매.getId(), 평가_안한_경매_판매자.getId()); + + // then + assertThat(actual).isFalse(); + } + + @Test + void 지정한_아이디가_평가_대상_아이디에_해당하는_평가_목록을_최신순으로_조회한다() { + // when + final List actual = reviewRepository.findAllByTargetId(구매자.getId()); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(2); + softAssertions.assertThat(actual.get(0)).isEqualTo(구매자가_판매자2에게_받은_평가); + softAssertions.assertThat(actual.get(1)).isEqualTo(구매자가_판매자1에게_받은_평가); + }); + } + + @Test + void 지정한_경매_아이디와_작성자_아이디가_해당하는_평가가_존재한다면_optional에_넣어_반환한다() { + // when + final Optional actual = + reviewRepository.findByAuctionIdAndWriterId(판매자1이_평가한_경매.getId(), 판매자1.getId()); + + // then + assertThat(actual).contains(구매자가_판매자1에게_받은_평가); + } + + @Test + void 지정한_경매_아이디와_작성자_아이디가_해당하는_평가가_존재하지_않는다면_빈_optional을_반환한다() { + // when + final Optional actual = + reviewRepository.findByAuctionIdAndWriterId(평가_안한_경매.getId(), 평가_안한_경매_판매자.getId()); + + // then + assertThat(actual).isEmpty(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/review/infrastructure/persistence/fixture/JpaReviewRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/review/infrastructure/persistence/fixture/JpaReviewRepositoryFixture.java new file mode 100644 index 000000000..fae340ff0 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/review/infrastructure/persistence/fixture/JpaReviewRepositoryFixture.java @@ -0,0 +1,156 @@ +package com.ddang.ddang.review.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.review.domain.Review; +import com.ddang.ddang.review.domain.Score; +import com.ddang.ddang.review.infrastructure.persistence.JpaReviewRepository; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class JpaReviewRepositoryFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaCategoryRepository categoryRepository; + + @Autowired + private JpaUserRepository userRepository; + + @Autowired + private JpaAuctionRepository auctionRepository; + + @Autowired + private JpaReviewRepository reviewRepository; + + + protected User 판매자1; + protected User 판매자2; + protected User 평가_안한_경매_판매자; + protected User 구매자; + protected Auction 판매자1이_평가한_경매; + protected Auction 판매자2가_평가한_경매; + protected Auction 평가_안한_경매; + protected Review 저장하려는_평가; + protected Review 구매자가_판매자1에게_받은_평가; + protected Review 구매자가_판매자2에게_받은_평가; + + @BeforeEach + void setUp() { + final Category 전자기기_카테고리 = new Category("전자기기"); + final Category 전자기기_서브_노트북_카테고리 = new Category("노트북 카테고리"); + 전자기기_카테고리.addSubCategory(전자기기_서브_노트북_카테고리); + categoryRepository.save(전자기기_카테고리); + + final ProfileImage 평가_안한_판매자_프로필_이미지 = new ProfileImage("no_review_seller_profile.png", "no_review_seller_profile.png"); + final ProfileImage 판매자1_프로필_이미지 = new ProfileImage("seller1_profile.png", "seller1_profile.png"); + final ProfileImage 판매자2_프로필_이미지 = new ProfileImage("seller2_profile.png", "seller2_profile.png"); + final ProfileImage 구매자_프로필_이미지 = new ProfileImage("buyer_profile.png", "buyer_profile.png"); + + 판매자1 = User.builder() + .name("판매자1") + .profileImage(판매자1_프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + 판매자2 = User.builder() + .name("판매자2") + .profileImage(판매자2_프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + 평가_안한_경매_판매자 = User.builder() + .name("평가 안한 판매자") + .profileImage(평가_안한_판매자_프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + 구매자 = User.builder() + .name("구매자") + .profileImage(구매자_프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + userRepository.saveAll(List.of(판매자1, 판매자2, 평가_안한_경매_판매자, 구매자)); + + final AuctionImage 경매1_대표_이미지 = new AuctionImage("경매1_대표_이미지.png", "경매1_대표_이미지.png"); + final AuctionImage 경매2_대표_이미지 = new AuctionImage("경매2_대표_이미지.png", "경매2_대표_이미지.png"); + final AuctionImage 평가_안한_경매_대표_이미지 = new AuctionImage("평가_안한_경매_대표_이미지.png", "평가_안한_경매_대표_이미지.png"); + + 판매자1이_평가한_경매 = Auction.builder() + .seller(판매자1) + .title("맥북") + .description("맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 판매자2가_평가한_경매 = Auction.builder() + .seller(판매자2) + .title("맥북") + .description("맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 평가_안한_경매 = Auction.builder() + .seller(판매자2) + .title("맥북") + .description("맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + + 판매자1이_평가한_경매.addAuctionImages(List.of(경매1_대표_이미지)); + 판매자2가_평가한_경매.addAuctionImages(List.of(경매2_대표_이미지)); + 평가_안한_경매.addAuctionImages(List.of(평가_안한_경매_대표_이미지)); + auctionRepository.saveAll(List.of(판매자1이_평가한_경매, 판매자2가_평가한_경매, 평가_안한_경매)); + + 저장하려는_평가 = Review.builder() + .auction(평가_안한_경매) + .writer(판매자1) + .target(구매자) + .content("친절하다.") + .score(new Score(5.0d)) + .build(); + 구매자가_판매자1에게_받은_평가 = Review.builder() + .auction(판매자1이_평가한_경매) + .writer(판매자1) + .target(구매자) + .content("친절하다.") + .score(new Score(5.0d)) + .build(); + 구매자가_판매자2에게_받은_평가 = Review.builder() + .auction(판매자2가_평가한_경매) + .writer(판매자2) + .target(구매자) + .content("별로다.") + .score(new Score(1.0d)) + .build(); + reviewRepository.saveAll(List.of(구매자가_판매자1에게_받은_평가, 구매자가_판매자2에게_받은_평가)); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/review/presentation/ReviewControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/review/presentation/ReviewControllerTest.java new file mode 100644 index 000000000..c81faf74a --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/review/presentation/ReviewControllerTest.java @@ -0,0 +1,238 @@ +package com.ddang.ddang.review.presentation; + +import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; +import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; +import com.ddang.ddang.authentication.domain.TokenDecoder; +import com.ddang.ddang.authentication.domain.TokenType; +import com.ddang.ddang.authentication.domain.dto.AuthenticationStore; +import com.ddang.ddang.exception.GlobalExceptionHandler; +import com.ddang.ddang.review.application.dto.CreateReviewDto; +import com.ddang.ddang.review.application.exception.AlreadyReviewException; +import com.ddang.ddang.review.application.exception.ReviewNotFoundException; +import com.ddang.ddang.review.presentation.fixture.ReviewControllerFixture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.List; +import java.util.Optional; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SuppressWarnings("NonAsciiCharacters") +class ReviewControllerTest extends ReviewControllerFixture { + + TokenDecoder tokenDecoder; + MockMvc mockMvc; + + @BeforeEach + void setUp() { + tokenDecoder = mock(TokenDecoder.class); + + final AuthenticationStore store = new AuthenticationStore(); + final AuthenticationInterceptor interceptor = new AuthenticationInterceptor( + blackListTokenService, + authenticationUserService, + tokenDecoder, + store + ); + final AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(store); + + mockMvc = MockMvcBuilders.standaloneSetup(reviewController) + .setControllerAdvice(new GlobalExceptionHandler()) + .addInterceptors(interceptor) + .setCustomArgumentResolvers(resolver) + .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) + .alwaysDo(print()) + .alwaysDo(restDocs) + .build(); + } + + @Test + void 평가를_등록한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(유효한_작성자_비공개_클레임)); + given(reviewService.create(any(CreateReviewDto.class))).willReturn(생성된_평가_아이디); + + // when & then + final ResultActions resultActions = + mockMvc.perform(RestDocumentationRequestBuilders.post("/reviews") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(사용자_평가_등록_요청)) + ).andExpectAll( + status().isCreated(), + header().string(HttpHeaders.LOCATION, is("/reviews/" + 생성된_평가_아이디)) + ); + + create_문서화(resultActions); + } + + @Test + void 이미_평가를_등록했다면_400를_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(유효한_작성자_비공개_클레임)); + given(reviewService.create(any(CreateReviewDto.class))).willThrow(new AlreadyReviewException("이미 평가하였습니다.")); + + // when & then + mockMvc.perform(post("/reviews") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(중복된_평가_등록_요청)) + ).andExpectAll( + status().isBadRequest(), + jsonPath("$.message").exists() + ); + } + + @Test + void 주어진_사용자가_받은_평가_목록을_최신순으로_조회한다() throws Exception { + // given + given(reviewService.readAllByTargetId(anyLong())) + .willReturn(List.of(구매자가_판매자2에게_받은_평가, 구매자가_판매자1에게_받은_평가)); + + // when & then + final ResultActions resultActions = + mockMvc.perform(RestDocumentationRequestBuilders.get("/reviews/users/{userId}", String.valueOf(구매자.id())) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpectAll( + status().isOk(), + jsonPath("$.[0].id", is(구매자가_판매자2에게_받은_평가.id()), Long.class), + jsonPath("$.[0].writer.id", is(구매자가_판매자2에게_받은_평가.writer().id()), Long.class), + jsonPath("$.[0].writer.name", is(구매자가_판매자2에게_받은_평가.writer().name())), + jsonPath("$.[0].content", is(구매자가_판매자2에게_받은_평가.content())), + jsonPath("$.[0].score", is(구매자가_판매자2에게_받은_평가.score())), + jsonPath("$.[1].id", is(구매자가_판매자1에게_받은_평가.id()), Long.class), + jsonPath("$.[1].writer.id", is(구매자가_판매자1에게_받은_평가.writer().id()), Long.class), + jsonPath("$.[1].writer.name", is(구매자가_판매자1에게_받은_평가.writer().name())), + jsonPath("$.[1].content", is(구매자가_판매자1에게_받은_평가.content())), + jsonPath("$.[1].score", is(구매자가_판매자1에게_받은_평가.score())) + ); + + readAllReviewsOfTargetUser_문서화(resultActions); + } + + @Test + void 지정한_평가_아이디에_해당하는_평가를_조회한다() throws Exception { + given(reviewService.readByReviewId(anyLong())) + .willReturn(구매자가_판매자1에게_받은_평가_내용); + + // when & then + final ResultActions resultActions = + mockMvc.perform(RestDocumentationRequestBuilders.get("/reviews/{reviewId}", 구매자가_판매자1에게_받은_평가_아이디) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpectAll( + status().isOk(), + jsonPath("score", is(구매자가_판매자1에게_받은_평가_내용.score()), Double.class), + jsonPath("content", is(구매자가_판매자1에게_받은_평가_내용.content())) + ); + + read_문서화(resultActions); + } + + @Test + void 지정한_평가_아이디에_해당하는_평가가_존재하지_않는다면_404가_발생한다() throws Exception { + given(reviewService.readByReviewId(anyLong())) + .willThrow(new ReviewNotFoundException("해당 평가를 찾을 수 없습니다.")); + + // when & then + mockMvc.perform(RestDocumentationRequestBuilders.get("/reviews/{reviewId}", 구매자가_판매자1에게_받은_평가_아이디) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpectAll( + status().isNotFound(), + jsonPath("message").exists() + ); + } + + private void create_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("회원 Bearer 인증 정보") + ), + requestFields( + fieldWithPath("auctionId").type(JsonFieldType.NUMBER) + .description("거래한 경매 ID"), + fieldWithPath("targetId").type(JsonFieldType.NUMBER) + .description("평가 대상 ID"), + fieldWithPath("score").type(JsonFieldType.NUMBER) + .description("평가 점수"), + fieldWithPath("content").type(JsonFieldType.STRING) + .description("평가 내용") + ) + ) + ); + } + + private void readAllReviewsOfTargetUser_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + pathParameters( + parameterWithName("userId").description("평가 목록 조회 대상 유저의 아이디") + ), + responseFields( + fieldWithPath("[]").type(JsonFieldType.ARRAY) + .description("조회 대상 사용자가 받은 모든 평가 목록"), + fieldWithPath("[].id").type(JsonFieldType.NUMBER) + .description("사용자 평가 ID"), + fieldWithPath("[].writer.id").type(JsonFieldType.NUMBER) + .description("평가를 작성한 사용자의 ID"), + fieldWithPath("[].writer.name").type(JsonFieldType.STRING) + .description("평가를 작성한 사용자의 이름"), + fieldWithPath("[].writer.profileImage").type(JsonFieldType.STRING) + .description("평가를 작성한 사용자의 프로필 이미지 url"), + fieldWithPath("[].content").type(JsonFieldType.STRING) + .description("평가 내용"), + fieldWithPath("[].score").type(JsonFieldType.NUMBER) + .description("평가 점수"), + fieldWithPath("[].createdTime").type(JsonFieldType.STRING) + .description("평가 작성 시간") + ) + ) + ); + } + + private void read_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + pathParameters( + parameterWithName("reviewId").description("평가와_관련된_경매_아이디") + ), + responseFields( + fieldWithPath("score").type(JsonFieldType.NUMBER) + .description("평가 점수"), + fieldWithPath("content").type(JsonFieldType.STRING) + .description("평가 내용") + ) + ) + ); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/review/presentation/dto/response/ReadReviewDetailResponseTest.java b/backend/ddang/src/test/java/com/ddang/ddang/review/presentation/dto/response/ReadReviewDetailResponseTest.java new file mode 100644 index 000000000..8aa85d9da --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/review/presentation/dto/response/ReadReviewDetailResponseTest.java @@ -0,0 +1,27 @@ +package com.ddang.ddang.review.presentation.dto.response; + +import com.ddang.ddang.review.application.dto.ReadReviewDetailDto; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class ReadReviewDetailResponseTest { + + @Test + void dto의_필드가_null일_때_response의_필드도_null이다() { + // given + final ReadReviewDetailDto readReviewDetailDto = new ReadReviewDetailDto(null, null); + + // when + final ReadReviewDetailResponse actual = ReadReviewDetailResponse.from(readReviewDetailDto); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual.score()).isNull(); + softAssertions.assertThat(actual.content()).isNull(); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/review/presentation/fixture/ReviewControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/review/presentation/fixture/ReviewControllerFixture.java new file mode 100644 index 000000000..d34dce198 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/review/presentation/fixture/ReviewControllerFixture.java @@ -0,0 +1,42 @@ +package com.ddang.ddang.review.presentation.fixture; + +import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims; +import com.ddang.ddang.configuration.CommonControllerSliceTest; +import com.ddang.ddang.review.application.dto.ReadReviewDetailDto; +import com.ddang.ddang.review.application.dto.ReadReviewDto; +import com.ddang.ddang.review.application.dto.ReadUserInReviewDto; +import com.ddang.ddang.review.presentation.dto.request.CreateReviewRequest; + +import java.time.LocalDateTime; + +@SuppressWarnings("NonAsciiCharacters") +public class ReviewControllerFixture extends CommonControllerSliceTest { + + private Long 유효한_평가_작성자_아이디 = 1L; + private Long 유효한_평가_대상_아이디 = 2L; + private Long 유효한_경매_아이디 = 1L; + private Long 사용자가_이미_평가한_경매_아이디 = 2L; + private Long 판매자1_프로필_이미지_아이디 = 1L; + private Long 판매자2_프로필_이미지_아이디 = 2L; + private Long 구매자_프로필_이미지_아이디 = 3L; + private ReadUserInReviewDto 판매자1 = new ReadUserInReviewDto(1L, "판매자1", 판매자1_프로필_이미지_아이디, 5.0d, "12347"); + private ReadUserInReviewDto 판매자2 = new ReadUserInReviewDto(2L, "판매자2", 판매자2_프로필_이미지_아이디, 5.0d, "12348"); + protected ReadUserInReviewDto 구매자 = new ReadUserInReviewDto(3L, "구매자", 구매자_프로필_이미지_아이디, 5.0d, "12349"); + protected String 액세스_토큰 = "Bearer accessToken"; + protected PrivateClaims 유효한_작성자_비공개_클레임 = new PrivateClaims(유효한_평가_작성자_아이디); + protected CreateReviewRequest 사용자_평가_등록_요청 = + new CreateReviewRequest(유효한_경매_아이디, 유효한_평가_대상_아이디, "친절하다.", 5.0f); + protected CreateReviewRequest 중복된_평가_등록_요청 = + new CreateReviewRequest(사용자가_이미_평가한_경매_아이디, 유효한_평가_대상_아이디, "친절하다.", 5.0f); + protected Long 생성된_평가_아이디 = 1L; + protected Long 구매자가_판매자1에게_받은_평가_아이디 = 2L; + protected Long 구매자가_판매자2에게_받은_평가_아이디 = 3L; + protected ReadReviewDto 구매자가_판매자1에게_받은_평가 = + new ReadReviewDto(구매자가_판매자1에게_받은_평가_아이디, 판매자1, "친절하다.", 5.0d, LocalDateTime.now()); + protected ReadReviewDto 구매자가_판매자2에게_받은_평가 = + new ReadReviewDto(구매자가_판매자2에게_받은_평가_아이디, 판매자2, "친절하다.", 5.0d, LocalDateTime.now()); + protected ReadReviewDetailDto 구매자가_판매자1에게_받은_평가_내용 = new ReadReviewDetailDto( + 구매자가_판매자1에게_받은_평가.score(), + 구매자가_판매자1에게_받은_평가.content() + ); +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/user/application/UserServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/user/application/UserServiceTest.java index f70b19b0b..6db02f940 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/user/application/UserServiceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/user/application/UserServiceTest.java @@ -1,114 +1,105 @@ package com.ddang.ddang.user.application; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - import com.ddang.ddang.configuration.IsolateDatabase; +import com.ddang.ddang.image.domain.StoreImageProcessor; import com.ddang.ddang.user.application.dto.ReadUserDto; import com.ddang.ddang.user.application.exception.UserNotFoundException; -import com.ddang.ddang.user.domain.User; -import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; -import java.util.Optional; +import com.ddang.ddang.user.application.fixture.UserServiceFixture; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; @IsolateDatabase @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class UserServiceTest { +class UserServiceTest extends UserServiceFixture { @Autowired UserService userService; - @Autowired - JpaUserRepository userRepository; + @MockBean + StoreImageProcessor imageProcessor; @Test void 특정_사용자_정보를_조회한다() { - // given - final User user = User.builder() - .name("사용자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(user); - // when - final ReadUserDto actual = userService.readById(user.getId()); + final ReadUserDto actual = userService.readById(사용자.getId()); // then SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual.name()).isEqualTo(user.getName()); - softAssertions.assertThat(actual.profileImage()).isEqualTo(user.getProfileImage()); - softAssertions.assertThat(actual.reliability()).isEqualTo(user.getReliability()); + softAssertions.assertThat(actual.name()).isEqualTo(사용자.getName()); + softAssertions.assertThat(actual.profileImageId()).isEqualTo(사용자.getProfileImage().getId()); + softAssertions.assertThat(actual.reliability()).isEqualTo(사용자.getReliability().getValue()); }); } @Test void 존재하지_않는_사용자_정보_조회시_예외를_반환한다() { - // given - final Long invalidUserId = -999L; - // when & then - assertThatThrownBy(() -> userService.readById(invalidUserId)) + assertThatThrownBy(() -> userService.readById(존재하지_않는_사용자_아이디)) .isInstanceOf(UserNotFoundException.class) .hasMessage("사용자 정보를 사용할 수 없습니다."); } @Test - void 회원_탈퇴한다() { + void 사용자_정보를_수정한다() { // given - final User user = User.builder() - .name("사용자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(user); + given(imageProcessor.storeImageFile(any())).willReturn(새로운_프로필_이미지_dto); // when - userService.deleteById(user.getId()); + userService.updateById(사용자.getId(), 사용자_정보_수정_요청_dto); // then - final Optional actual = userRepository.findById(user.getId()); + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(사용자.getName()).isEqualTo(사용자_정보_수정_요청_dto.name()); + softAssertions.assertThat(사용자.getProfileImage().getImage().getStoreName()) + .isEqualTo(새로운_프로필_이미지_dto.storeName()); + softAssertions.assertThat(사용자.getProfileImage().getImage().getUploadName()) + .isEqualTo(새로운_프로필_이미지_dto.uploadName()); + }); + } + @Test + void 사용자_정보를_수정시_이름만_수정한다() { + // when + userService.updateById(사용자.getId(), 사용자_이름만_수정_요청_dto); + + // then SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual).isPresent(); - softAssertions.assertThat(actual.get().isDeleted()).isTrue(); + softAssertions.assertThat(사용자.getName()).isEqualTo(사용자_이름만_수정_요청_dto.name()); + softAssertions.assertThat(사용자.getProfileImage()).isEqualTo(프로필_이미지); }); } @Test - void 회원_탈퇴할때_이미_탈퇴한_회원이면_예외가_발생한다() { + void 사용자_정보를_수정시_이미지만_수정한다() { // given - final User user = User.builder() - .name("사용자") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); + given(imageProcessor.storeImageFile(any())).willReturn(새로운_프로필_이미지_dto); - user.withdrawal(); - userRepository.save(user); + // when + userService.updateById(사용자.getId(), 사용자_이미지만_수정_요청_dto); - // when & then - assertThatThrownBy(() -> userService.deleteById(user.getId())) - .isInstanceOf(UserNotFoundException.class) - .hasMessage("사용자 정보를 사용할 수 없습니다."); + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(사용자.getName()).isEqualTo(사용자_이름); + softAssertions.assertThat(사용자.getProfileImage().getImage().getStoreName()) + .isEqualTo(새로운_프로필_이미지_dto.storeName()); + softAssertions.assertThat(사용자.getProfileImage().getImage().getUploadName()) + .isEqualTo(새로운_프로필_이미지_dto.uploadName()); + }); } @Test - void 회원_탈퇴할때_존재하지_않는_사용자_정보_조회시_예외를_반환한다() { - // given - final Long invalidUserId = -999L; - + void 사용자_정보_수정시_존재하지_않는_사용자라면_예외가_발생한다() { // when & then - assertThatThrownBy(() -> userService.deleteById(invalidUserId)) + assertThatThrownBy(() -> userService.updateById(존재하지_않는_사용자_아이디, 사용자_정보_수정_요청_dto)) .isInstanceOf(UserNotFoundException.class) .hasMessage("사용자 정보를 사용할 수 없습니다."); } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/user/application/fixture/UserServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/user/application/fixture/UserServiceFixture.java new file mode 100644 index 000000000..11a3a18aa --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/user/application/fixture/UserServiceFixture.java @@ -0,0 +1,54 @@ +package com.ddang.ddang.user.application.fixture; + +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.image.domain.dto.StoreImageDto; +import com.ddang.ddang.user.application.dto.UpdateUserDto; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; + +@SuppressWarnings("NonAsciiCharacters") +public class UserServiceFixture { + + @Autowired + private JpaUserRepository userRepository; + + protected Long 존재하지_않는_사용자_아이디 = -999L; + + protected String 사용자_이름 = "사용자"; + protected ProfileImage 프로필_이미지; + protected User 사용자; + protected StoreImageDto 새로운_프로필_이미지_dto; + protected UpdateUserDto 사용자_정보_수정_요청_dto; + protected UpdateUserDto 사용자_이름만_수정_요청_dto; + protected UpdateUserDto 사용자_이미지만_수정_요청_dto; + + @BeforeEach + void setUp() { + 프로필_이미지 = new ProfileImage("upload.png", "store.png"); + 사용자 = User.builder() + .name(사용자_이름) + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + + userRepository.save(사용자); + + final MockMultipartFile 새로운_이미지_파일 = new MockMultipartFile( + "profileImage", + "updateImage.png", + MediaType.IMAGE_PNG.toString(), + new byte[]{1} + ); + 새로운_프로필_이미지_dto = new StoreImageDto(새로운_이미지_파일.getOriginalFilename(), "newStore.png"); + + 사용자_정보_수정_요청_dto = new UpdateUserDto("updateName", 새로운_이미지_파일); + 사용자_이름만_수정_요청_dto = new UpdateUserDto("updateName", null); + 사용자_이미지만_수정_요청_dto = new UpdateUserDto(null, 새로운_이미지_파일); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/user/domain/ReliabilityTest.java b/backend/ddang/src/test/java/com/ddang/ddang/user/domain/ReliabilityTest.java new file mode 100644 index 000000000..9aec3fc19 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/user/domain/ReliabilityTest.java @@ -0,0 +1,38 @@ +package com.ddang.ddang.user.domain; + +import com.ddang.ddang.review.domain.Review; +import com.ddang.ddang.user.domain.fixture.ReliabilityFixture; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class ReliabilityTest extends ReliabilityFixture { + + @Test + void 신뢰도_점수_평균을_계산한다() { + // when + 평가_대상.updateReliability(평가_대상이_받은_평가_목록); + + // then + assertThat(평가_대상.getReliability().getValue()).isEqualTo(평가_대상의_신뢰도_점수); + } + + @Test + void 신뢰도_기록이_없다면_신뢰도_점수는_null이다() { + // given + final List targetReviews = Collections.emptyList(); + + // when + 평가_대상.updateReliability(targetReviews); + + // then + assertThat(평가_대상.getReliability().getValue()).isNull(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/user/domain/UserTest.java b/backend/ddang/src/test/java/com/ddang/ddang/user/domain/UserTest.java index 6fac9f57e..9946a3d3b 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/user/domain/UserTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/user/domain/UserTest.java @@ -1,20 +1,103 @@ package com.ddang.ddang.user.domain; -import static org.assertj.core.api.Assertions.assertThat; - +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.fixture.UserFixture; +import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class UserTest { +class UserTest extends UserFixture { + + @Test + void 회원_생성시_신뢰도가_null이면_신뢰도_점수가_null인_신뢰도로_회원을_생성한다() { + // given + final Reliability nullReliability = null; + final Reliability expect = new Reliability(null); + + // when + final User user = User.builder() + .name("kakao12345") + .profileImage(new ProfileImage("uplad.png", "store.png")) + .reliability(nullReliability) + .oauthId("12345") + .build(); + + // then + assertThat(user.getReliability()).isEqualTo(expect); + } + + @Test + void 회원_생성시_신뢰도가_null이_아니라면_해당_신뢰도로_회원을_생성한다() { + // given + final Reliability notNullReliability = new Reliability(4.5d); + + // when + final User user = User.builder() + .name("kakao12345") + .profileImage(new ProfileImage("uplad.png", "store.png")) + .reliability(notNullReliability) + .oauthId("12345") + .build(); + + // then + assertThat(user.getReliability()).isEqualTo(notNullReliability); + } + + @Test + void 회원_정보의_이름을_수정한다() { + // given + final User user = User.builder() + .name("kakao12345") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(5.0d)) + .oauthId("12345") + .build(); + + // when + user.updateName("newName"); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(user.getName()).isEqualTo("newName"); + softAssertions.assertThat(user.getProfileImage()).isEqualTo(new ProfileImage("upload.png", "store.png")); + softAssertions.assertThat(user.getReliability().getValue()).isEqualTo(5.0d); + }); + } + + @Test + void 회원_정보의_이미지를_수정한다() { + // given + final User user = User.builder() + .name("kakao12345") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(5.0d)) + .oauthId("12345") + .build(); + + // when + user.updateProfileImage(new ProfileImage("updateUpload.png", "updateStore.png")); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(user.getName()).isEqualTo("kakao12345"); + softAssertions.assertThat(user.getProfileImage()).isEqualTo(new ProfileImage("updateUpload.png", "updateStore.png")); + softAssertions.assertThat(user.getReliability().getValue()).isEqualTo(5.0d); + }); + } @Test void 회원_탈퇴한다() { // given final User user = User.builder() .name("kakao12345") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(5.0d)) + .oauthId("12345") .build(); // when @@ -23,4 +106,13 @@ class UserTest { // then assertThat(user.isDeleted()).isTrue(); } + + @Test + void 신뢰도_평균을_계산한다() { + // when + 평가_대상.updateReliability(평가_대상이_받은_평가_목록); + + // then + assertThat(평가_대상.getReliability().getValue()).isEqualTo(평가_대상의_신뢰도_점수); + } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/user/domain/fixture/ReliabilityFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/user/domain/fixture/ReliabilityFixture.java new file mode 100644 index 000000000..bd0ff2e2d --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/user/domain/fixture/ReliabilityFixture.java @@ -0,0 +1,50 @@ +package com.ddang.ddang.user.domain.fixture; + +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.review.domain.Review; +import com.ddang.ddang.review.domain.Score; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; + +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class ReliabilityFixture { + + private double 평가1_점수 = 5.0d; + private double 평가2_점수 = 1.0d; + private User 평가_작성자1 = User.builder() + .name("평가 작성자1") + .profileImage(new ProfileImage("profile.png", "profile.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + private User 평가_작성자2 = User.builder() + .name("평가 작성자2") + .profileImage(new ProfileImage("profile.png", "profile.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + + protected User 평가_대상 = User.builder() + .name("평가 대상") + .profileImage(new ProfileImage("profile.png", "profile.png")) + .reliability(new Reliability(null)) + .oauthId("12345") + .build(); + private Review 평가1 = Review.builder() + .writer(평가_작성자1) + .target(평가_대상) + .content("친절하다") + .score(new Score(평가1_점수)) + .build(); + private Review 평가2 = Review.builder() + .writer(평가_작성자2) + .target(평가_대상) + .content("별로다") + .score(new Score(평가2_점수)) + .build(); + + protected List 평가_대상이_받은_평가_목록 = List.of(평가1, 평가2); + protected Double 평가_대상의_신뢰도_점수 = (평가1_점수 + 평가2_점수) / 2; +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/user/domain/fixture/UserFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/user/domain/fixture/UserFixture.java new file mode 100644 index 000000000..44c558433 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/user/domain/fixture/UserFixture.java @@ -0,0 +1,50 @@ +package com.ddang.ddang.user.domain.fixture; + +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.review.domain.Review; +import com.ddang.ddang.review.domain.Score; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; + +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class UserFixture { + + private double 평가1_점수 = 5.0d; + private double 평가2_점수 = 1.0d; + private User 평가_작성자1 = User.builder() + .name("평가 작성자1") + .profileImage(new ProfileImage("profile.png", "profile.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + private User 평가_작성자2 = User.builder() + .name("평가 작성자2") + .profileImage(new ProfileImage("profile.png", "profile.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + + protected User 평가_대상 = User.builder() + .name("평가 대상") + .profileImage(new ProfileImage("profile.png", "profile.png")) + .reliability(new Reliability(null)) + .oauthId("12345") + .build(); + private Review 평가1 = Review.builder() + .writer(평가_작성자1) + .target(평가_대상) + .content("친절하다") + .score(new Score(평가1_점수)) + .build(); + private Review 평가2 = Review.builder() + .writer(평가_작성자2) + .target(평가_대상) + .content("별로다") + .score(new Score(평가2_점수)) + .build(); + + protected List 평가_대상이_받은_평가_목록 = List.of(평가1, 평가2); + protected Double 평가_대상의_신뢰도_점수 = (평가1_점수 + 평가2_점수) / 2; +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/user/infrastructure/persistence/JpaUserRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/user/infrastructure/persistence/JpaUserRepositoryTest.java index 8b7e2a650..c6564ec79 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/user/infrastructure/persistence/JpaUserRepositoryTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/user/infrastructure/persistence/JpaUserRepositoryTest.java @@ -1,15 +1,12 @@ package com.ddang.ddang.user.infrastructure.persistence; -import static org.assertj.core.api.Assertions.assertThat; - import com.ddang.ddang.configuration.JpaConfiguration; import com.ddang.ddang.configuration.QuerydslConfiguration; +import com.ddang.ddang.user.domain.Reliability; import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.fixture.JpaUserRepositoryFixture; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; - -import java.util.Optional; - import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -17,11 +14,15 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + @DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -@Import({JpaConfiguration.class, QuerydslConfiguration.class}) -class JpaUserRepositoryTest { +class JpaUserRepositoryTest extends JpaUserRepositoryFixture { @PersistenceContext EntityManager em; @@ -30,34 +31,38 @@ class JpaUserRepositoryTest { JpaUserRepository userRepository; @Test - void 존재하는_oauthId를_전달하면_해당_회원을_Optional로_감싸_반환한다() { + void 사용자를_저장한다() { // given final User user = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) + .name("새로운 사용자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) .oauthId("12345") .build(); - userRepository.save(user); + // when + final User actual = userRepository.save(user); + // then em.flush(); em.clear(); + assertThat(actual.getId()).isPositive(); + } + + @Test + void 존재하는_oauthId를_전달하면_해당_회원을_Optional로_감싸_반환한다() { // when - final Optional actual = userRepository.findByOauthId(user.getOauthId()); + final Optional actual = userRepository.findByOauthIdAndDeletedIsFalse(사용자.getOauthId()); // then - assertThat(actual).isPresent(); + assertThat(actual).contains(사용자); } @Test void 존재하지_않는_oauthId를_전달하면_해당_회원을_빈_Optional로_반환한다() { - // given - final String invalidOauthId = "invalidOauthId"; - // when - final Optional actual = userRepository.findByOauthId(invalidOauthId); + final Optional actual = userRepository.findByOauthIdAndDeletedIsFalse(존재하지_않는_oauth_아이디); // then assertThat(actual).isEmpty(); @@ -65,44 +70,17 @@ class JpaUserRepositoryTest { @Test void 회원가입과_탈퇴하지_않은_회원_id를_전달하면_해당_회원을_Optional로_감싸_반환한다() { - // given - final User user = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(user); - - em.flush(); - em.clear(); - // when - final Optional actual = userRepository.findByIdAndDeletedIsFalse(user.getId()); + final Optional actual = userRepository.findByIdAndDeletedIsFalse(사용자.getId()); // then - assertThat(actual).isPresent(); + assertThat(actual).contains(사용자); } @Test void 회원탈퇴한_회원의_id를_전달하면_빈_Optional을_반환한다() { - // given - final User user = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - user.withdrawal(); - userRepository.save(user); - - em.flush(); - em.clear(); - // when - final Optional actual = userRepository.findByIdAndDeletedIsFalse(user.getId()); + final Optional actual = userRepository.findByIdAndDeletedIsFalse(탈퇴한_사용자.getId()); // then assertThat(actual).isEmpty(); @@ -110,11 +88,8 @@ class JpaUserRepositoryTest { @Test void 없는_id를_전달하면_빈_Optional을_반환한다() { - // given - final Long invalidUserId = -999L; - // when - final Optional actual = userRepository.findByIdAndDeletedIsFalse(invalidUserId); + final Optional actual = userRepository.findByIdAndDeletedIsFalse(존재하지_않는_사용자_아이디); // then assertThat(actual).isEmpty(); @@ -122,22 +97,8 @@ class JpaUserRepositoryTest { @Test void 회원탈퇴한_회원의_id를_전달하면_참을_반환한다() { - // given - final User user = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - user.withdrawal(); - userRepository.save(user); - - em.flush(); - em.clear(); - // when - final boolean actual = userRepository.existsByIdAndDeletedIsTrue(user.getId()); + final boolean actual = userRepository.existsByIdAndDeletedIsTrue(탈퇴한_사용자.getId()); // then assertThat(actual).isTrue(); @@ -145,23 +106,28 @@ class JpaUserRepositoryTest { @Test void 회원탈퇴하지_않거나_회원가입하지_않은_회원의_id를_전달하면_거짓을_반환한다() { - // given - final User user = User.builder() - .name("회원") - .profileImage("profile.png") - .reliability(4.7d) - .oauthId("12345") - .build(); - - userRepository.save(user); + // when + final boolean actual = userRepository.existsByIdAndDeletedIsTrue(사용자.getId()); - em.flush(); - em.clear(); + // then + assertThat(actual).isFalse(); + } + @Test + void 이름이_아직_없다면_거짓을_반환한다() { // when - final boolean actual = userRepository.existsByIdAndDeletedIsTrue(user.getId()); + final boolean actual = userRepository.existsByNameEndingWith(존재하지_않는_사용자_이름); // then assertThat(actual).isFalse(); } + + @Test + void 이름이_있다면_참을_반환한다() { + // when + final boolean actual = userRepository.existsByNameEndingWith(존재하는_사용자_이름); + + // then + assertThat(actual).isTrue(); + } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/user/infrastructure/persistence/fixture/JpaUserRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/user/infrastructure/persistence/fixture/JpaUserRepositoryFixture.java new file mode 100644 index 000000000..437b21aef --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/user/infrastructure/persistence/fixture/JpaUserRepositoryFixture.java @@ -0,0 +1,56 @@ +package com.ddang.ddang.user.infrastructure.persistence.fixture; + +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class JpaUserRepositoryFixture { + + @PersistenceContext + private EntityManager em; + + @Autowired + private JpaUserRepository userRepository; + + protected ProfileImage 프로필_이미지 = new ProfileImage("upload.png", "store.png"); + protected String 존재하지_않는_oauth_아이디 = "invalidOauthId"; + protected Long 존재하지_않는_사용자_아이디 = -999L; + protected String 존재하지_않는_사용자_이름 = "새로운 이름"; + protected String 존재하는_사용자_이름; + protected User 사용자; + protected User 탈퇴한_사용자; + + @BeforeEach + void setUp() { + final ProfileImage 사용자_프로필_이미지 = new ProfileImage("upload.png", "store.png"); + 사용자 = User.builder() + .name("사용자") + .profileImage(사용자_프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + + 탈퇴한_사용자 = User.builder() + .name("탈퇴한 사용자") + .profileImage(사용자_프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + 탈퇴한_사용자.withdrawal(); + + userRepository.saveAll(List.of(사용자, 탈퇴한_사용자)); + + 존재하는_사용자_이름 = 사용자.getName(); + + em.flush(); + em.clear(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/UserAuctionControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/UserAuctionControllerTest.java new file mode 100644 index 000000000..c6978a350 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/UserAuctionControllerTest.java @@ -0,0 +1,205 @@ +package com.ddang.ddang.user.presentation; + +import com.ddang.ddang.auction.configuration.DescendingSortPageableArgumentResolver; +import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; +import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; +import com.ddang.ddang.authentication.domain.TokenDecoder; +import com.ddang.ddang.authentication.domain.TokenType; +import com.ddang.ddang.authentication.domain.dto.AuthenticationStore; +import com.ddang.ddang.exception.GlobalExceptionHandler; +import com.ddang.ddang.user.presentation.fixture.UserAuctionControllerFixture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.Optional; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SuppressWarnings("NonAsciiCharacters") +class UserAuctionControllerTest extends UserAuctionControllerFixture { + + TokenDecoder tokenDecoder; + + MockMvc mockMvc; + + @BeforeEach + void setUp() { + tokenDecoder = mock(TokenDecoder.class); + + final AuthenticationStore store = new AuthenticationStore(); + final AuthenticationInterceptor interceptor = new AuthenticationInterceptor( + blackListTokenService, + authenticationUserService, + tokenDecoder, + store + ); + final AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(store); + + mockMvc = MockMvcBuilders.standaloneSetup(userAuctionController) + .setControllerAdvice(new GlobalExceptionHandler()) + .addInterceptors(interceptor) + .setCustomArgumentResolvers(resolver, new DescendingSortPageableArgumentResolver()) + .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) + .alwaysDo(print()) + .alwaysDo(restDocs) + .build(); + } + + @Test + void 로그인한_회원이_등록한_경매_목록을_조회한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(auctionService.readAllByUserId(anyLong(), any(Pageable.class))).willReturn(사용자의_경매들_정보_dto); + + // when & then + final ResultActions resultActions = + mockMvc.perform(get("/users/auctions/mine") + .contentType(MediaType.APPLICATION_JSON) + .queryParam("size", 페이지_크기) + .queryParam("page", 페이지) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + ) + .andExpectAll( + status().isOk(), + jsonPath("$.auctions.[0].id", is(경매_정보_dto2.id()), Long.class), + jsonPath("$.auctions.[0].title", is(경매_정보_dto2.title())), + jsonPath("$.auctions.[0].image").exists(), + jsonPath("$.auctions.[0].auctionPrice", is(경매_정보_dto2.startPrice())), + jsonPath("$.auctions.[0].status").exists(), + jsonPath("$.auctions.[0].auctioneerCount", is(경매_정보_dto2.auctioneerCount())), + jsonPath("$.auctions.[1].id", is(경매_정보_dto1.id()), Long.class), + jsonPath("$.auctions.[1].title", is(경매_정보_dto1.title())), + jsonPath("$.auctions.[1].image").exists(), + jsonPath("$.auctions.[1].auctionPrice", is(경매_정보_dto1.startPrice())), + jsonPath("$.auctions.[1].status").exists(), + jsonPath("$.auctions.[1].auctioneerCount", is(경매_정보_dto1.auctioneerCount())) + ); + + readAllByUserInfo_문서화(resultActions); + } + + @Test + void 로그인한_회원이_참여한_경매_목록을_조회한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(auctionService.readAllByBidderId(anyLong(), any(Pageable.class))).willReturn(사용자가_참여한_경매들_정보_dto); + + // when & then + final ResultActions resultActions = + mockMvc.perform(get("/users/auctions/bids") + .contentType(MediaType.APPLICATION_JSON) + .queryParam("size", 페이지_크기) + .queryParam("page", 페이지) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + ) + .andExpectAll( + status().isOk(), + jsonPath("$.auctions.[0].id", is(경매_정보_dto2.id()), Long.class), + jsonPath("$.auctions.[0].title", is(경매_정보_dto2.title())), + jsonPath("$.auctions.[0].image").exists(), + jsonPath("$.auctions.[0].auctionPrice", is(경매_정보_dto2.startPrice())), + jsonPath("$.auctions.[0].status").exists(), + jsonPath("$.auctions.[0].auctioneerCount", is(경매_정보_dto2.auctioneerCount())), + jsonPath("$.auctions.[1].id", is(경매_정보_dto1.id()), Long.class), + jsonPath("$.auctions.[1].title", is(경매_정보_dto1.title())), + jsonPath("$.auctions.[1].image").exists(), + jsonPath("$.auctions.[1].auctionPrice", is(경매_정보_dto1.startPrice())), + jsonPath("$.auctions.[1].status").exists(), + jsonPath("$.auctions.[1].auctioneerCount", is(경매_정보_dto1.auctioneerCount())) + ); + + readAllByBids_문서화(resultActions); + } + + private void readAllByUserInfo_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + queryParameters( + parameterWithName("size").description("페이지 크기").optional(), + parameterWithName("page").description("페이지 번호") + ), + responseFields( + fieldWithPath("auctions").type(JsonFieldType.ARRAY) + .description("조회한 경매 목록"), + fieldWithPath("auctions.[]").type(JsonFieldType.ARRAY) + .description("조회한 단일 경매 정보"), + fieldWithPath("auctions.[].id").type(JsonFieldType.NUMBER) + .description("경매 ID"), + fieldWithPath("auctions.[].title").type(JsonFieldType.STRING) + .description("경매 글 제목"), + fieldWithPath("auctions.[].image").type(JsonFieldType.STRING) + .description("경매 대표 이미지"), + fieldWithPath("auctions.[].auctionPrice").type(JsonFieldType.NUMBER) + .description("경매가(시작가, 현재가, 낙찰가 중 하나)"), + fieldWithPath("auctions.[].status").type(JsonFieldType.STRING) + .description("경매 상태"), + fieldWithPath("auctions.[].auctioneerCount").type(JsonFieldType.NUMBER) + .description("경매 참여자 수"), + fieldWithPath("isLast").type(JsonFieldType.BOOLEAN) + .description("마지막 페이지 여부") + ) + ) + ); + } + + private void readAllByBids_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + queryParameters( + parameterWithName("size").description("페이지 크기").optional(), + parameterWithName("page").description("페이지 번호") + ), + responseFields( + fieldWithPath("auctions").type(JsonFieldType.ARRAY) + .description("조회한 경매 목록"), + fieldWithPath("auctions.[]").type(JsonFieldType.ARRAY) + .description("조회한 단일 경매 정보"), + fieldWithPath("auctions.[].id").type(JsonFieldType.NUMBER) + .description("경매 ID"), + fieldWithPath("auctions.[].title").type(JsonFieldType.STRING) + .description("경매 글 제목"), + fieldWithPath("auctions.[].image").type(JsonFieldType.STRING) + .description("경매 대표 이미지"), + fieldWithPath("auctions.[].auctionPrice").type(JsonFieldType.NUMBER) + .description("경매가(시작가, 현재가, 낙찰가 중 하나)"), + fieldWithPath("auctions.[].status").type(JsonFieldType.STRING) + .description("경매 상태"), + fieldWithPath("auctions.[].auctioneerCount").type(JsonFieldType.NUMBER) + .description("경매 참여자 수"), + fieldWithPath("isLast").type(JsonFieldType.BOOLEAN) + .description("마지막 페이지 여부") + ) + ) + ); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/UserControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/UserControllerTest.java index 4cab74a64..83ca223a7 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/UserControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/UserControllerTest.java @@ -1,89 +1,61 @@ package com.ddang.ddang.user.presentation; -import static org.hamcrest.Matchers.is; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willDoNothing; -import static org.mockito.Mockito.mock; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.ddang.ddang.authentication.application.AuthenticationUserService; -import com.ddang.ddang.authentication.application.BlackListTokenService; import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; import com.ddang.ddang.authentication.domain.TokenDecoder; import com.ddang.ddang.authentication.domain.TokenType; import com.ddang.ddang.authentication.domain.dto.AuthenticationStore; -import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims; -import com.ddang.ddang.configuration.RestDocsConfiguration; import com.ddang.ddang.exception.GlobalExceptionHandler; -import com.ddang.ddang.user.application.UserService; -import com.ddang.ddang.user.application.dto.ReadUserDto; import com.ddang.ddang.user.application.exception.UserNotFoundException; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.util.Optional; +import com.ddang.ddang.user.presentation.fixture.UserControllerFixture; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; -import org.springframework.context.annotation.Import; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@WebMvcTest(controllers = {UserController.class}, - excludeFilters = { - @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfigurer.class), - @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com\\.ddang\\.ddang\\.authentication\\.configuration\\..*") - } -) -@AutoConfigureRestDocs -@Import(RestDocsConfiguration.class) -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -@SuppressWarnings("NonAsciiCharacters") -class UserControllerTest { - - @MockBean - UserService userService; - @MockBean - BlackListTokenService blackListTokenService; - - @MockBean - AuthenticationUserService authenticationUserService; +import java.util.Optional; - @Autowired - UserController userController; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @Autowired - ObjectMapper objectMapper; +@SuppressWarnings("NonAsciiCharacters") +class UserControllerTest extends UserControllerFixture { - TokenDecoder mockTokenDecoder; + TokenDecoder tokenDecoder; MockMvc mockMvc; @BeforeEach void setUp() { - mockTokenDecoder = mock(TokenDecoder.class); + tokenDecoder = mock(TokenDecoder.class); final AuthenticationStore store = new AuthenticationStore(); final AuthenticationInterceptor interceptor = new AuthenticationInterceptor( blackListTokenService, authenticationUserService, - mockTokenDecoder, + tokenDecoder, store ); final AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(store); @@ -92,63 +64,168 @@ void setUp() { .setControllerAdvice(new GlobalExceptionHandler()) .addInterceptors(interceptor) .setCustomArgumentResolvers(resolver) + .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) .alwaysDo(print()) + .alwaysDo(restDocs) .build(); } @Test void 사용자_정보를_조회한다() throws Exception { // given - final ReadUserDto readUserDto = new ReadUserDto(1L, "사용자1", "profile.png", 4.6d, "12345"); - final PrivateClaims privateClaims = new PrivateClaims(1L); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(userService.readById(anyLong())).willReturn(사용자_정보_조회_dto); - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(userService.readById(anyLong())).willReturn(readUserDto); + // when & then + final ResultActions resultActions = mockMvc.perform(get("/users") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + ) + .andExpectAll( + status().isOk(), + jsonPath("$.name", is(사용자_정보_조회_dto.name())), + jsonPath("$.profileImage").exists(), + jsonPath("$.reliability", is(사용자_정보_조회_dto.reliability())) + ); + + readById_문서화(resultActions); + } + + @Test + void 탈퇴한_사용자_정보를_조회한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(userService.readById(anyLong())).willReturn(탈퇴한_사용자_정보_조회_dto); // when & then mockMvc.perform(get("/users") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) ) .andExpectAll( status().isOk(), - jsonPath("$.name", is(readUserDto.name())), - jsonPath("$.profileImage", is(readUserDto.profileImage())), - jsonPath("$.reliability", is(readUserDto.reliability())) + jsonPath("$.name", is(탈퇴한_사용자_이름)), + jsonPath("$.profileImage").exists(), + jsonPath("$.reliability", is(탈퇴한_사용자_정보_조회_dto.reliability())) ); } @Test - void 존재하지_않는_사용자_정보_조회시_404를_반환한다() throws Exception { + void 사용자_정보를_모두_수정한다() throws Exception { // given - final UserNotFoundException userNotFoundException = new UserNotFoundException("사용자 정보를 사용할 수 없습니다."); - final PrivateClaims privateClaims = new PrivateClaims(1L); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(userService.updateById(anyLong(), any())).willReturn(수정후_사용자_정보_조회_dto); + + // when & then + final ResultActions resultActions = mockMvc.perform(multipart(HttpMethod.PATCH, "/users") + .file(수정할_이름) + .file(수정할_프로필_이미지) + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + ) + .andExpectAll( + status().isOk(), + jsonPath("$.name", is(수정할_이름_request.name())), + jsonPath("$.profileImage").exists(), + jsonPath("$.reliability", is(사용자_정보_조회_dto.reliability())) + ); + + updateById_문서화(resultActions); + } - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - given(userService.readById(anyLong())).willThrow(userNotFoundException); + @Test + void 사용자_정보를_이름만_수정한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(userService.updateById(anyLong(), any())).willReturn(수정후_사용자_정보_조회_dto); // when & then - mockMvc.perform(get("/users") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + mockMvc.perform(multipart(HttpMethod.PATCH, "/users") + .file(수정할_이름) + .file(프로필_이미지가_없는_경우_파일) + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) ) .andExpectAll( - status().isNotFound(), - jsonPath("$.message", is(userNotFoundException.getMessage())) + status().isOk(), + jsonPath("$.name", is(수정할_이름_request.name())), + jsonPath("$.profileImage").exists(), + jsonPath("$.reliability", is(사용자_정보_조회_dto.reliability())) ); } @Test - void 회원_탈퇴한다() throws Exception { + void 사용자_정보를_이미지만_수정한다() throws Exception { // given - final PrivateClaims privateClaims = new PrivateClaims(1L); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(userService.updateById(anyLong(), any())).willReturn(사용자_정보_조회_dto); + + // when & then + mockMvc.perform(multipart(HttpMethod.PATCH, "/users") + .file(이름을_수정하지_않는_경우) + .file(수정할_프로필_이미지) + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + ) + .andExpectAll( + status().isOk(), + jsonPath("$.name", is(사용자_정보_조회_dto.name())), + jsonPath("$.profileImage").exists(), + jsonPath("$.reliability", is(사용자_정보_조회_dto.reliability())) + ); + } - given(mockTokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(privateClaims)); - willDoNothing().given(userService).deleteById(anyLong()); + @Test + void 존재하지_않는_사용자_정보_조회시_404를_반환한다() throws Exception { + // given + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); + given(userService.readById(anyLong())).willThrow(new UserNotFoundException("사용자 정보를 사용할 수 없습니다.")); // when & then - mockMvc.perform(delete("/users/withdrawal") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - ).andExpectAll( - status().isNoContent() + mockMvc.perform(get("/users") + .header(HttpHeaders.AUTHORIZATION, 액세스_토큰_값) + ) + .andExpectAll( + status().isNotFound(), + jsonPath("$.message").exists() + ); + } + + private void readById_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + responseFields( + fieldWithPath("name").type(JsonFieldType.STRING) + .description("사용자 닉네임"), + fieldWithPath("profileImage").type(JsonFieldType.STRING) + .description("사용자 프로필 이미지"), + fieldWithPath("reliability").type(JsonFieldType.NUMBER) + .description("사용자 신뢰도") + ) + ) + ); + } + + private void updateById_문서화(final ResultActions resultActions) throws Exception { + resultActions.andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("회원 Bearer 인증 정보") + ), + requestParts( + partWithName("profileImage").description("수정할 프로필 이미지 파일"), + partWithName("request").description("요청 데이터 - 수정할 이름") + ), + responseFields( + fieldWithPath("name").type(JsonFieldType.STRING) + .description("사용자 닉네임"), + fieldWithPath("profileImage").type(JsonFieldType.STRING) + .description("사용자 프로필 이미지"), + fieldWithPath("reliability").type(JsonFieldType.NUMBER) + .description("사용자 신뢰도") + ) + ) ); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/fixture/UserAuctionControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/fixture/UserAuctionControllerFixture.java new file mode 100644 index 000000000..506a674df --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/fixture/UserAuctionControllerFixture.java @@ -0,0 +1,73 @@ +package com.ddang.ddang.user.presentation.fixture; + +import com.ddang.ddang.auction.application.dto.ReadAuctionDto; +import com.ddang.ddang.auction.application.dto.ReadAuctionsDto; +import com.ddang.ddang.auction.application.dto.ReadRegionDto; +import com.ddang.ddang.auction.application.dto.ReadRegionsDto; +import com.ddang.ddang.auction.domain.AuctionStatus; +import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims; +import com.ddang.ddang.configuration.CommonControllerSliceTest; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class UserAuctionControllerFixture extends CommonControllerSliceTest { + + protected String 액세스_토큰_값 = "Bearer accessToken"; + protected PrivateClaims 사용자_ID_클레임 = new PrivateClaims(1L); + protected String 페이지_크기 = "10"; + protected String 페이지 = "1"; + + final ReadRegionsDto 직거래_지역_정보_dto = new ReadRegionsDto( + new ReadRegionDto(1L, "서울특별시"), + new ReadRegionDto(2L, "강서구"), + new ReadRegionDto(3L, "역삼동") + ); + protected ReadAuctionDto 경매_정보_dto1 = new ReadAuctionDto( + 1L, + "경매 상품 1", + "이것은 경매 상품 1 입니다.", + 1_000, + 1_000, + null, + false, + LocalDateTime.now(), + LocalDateTime.now(), + List.of(직거래_지역_정보_dto), + List.of(1L), + 2, + "main1", + "sub1", + 1L, + 1L, + "판매자", + 3.5d, + false, + AuctionStatus.UNBIDDEN + ); + protected ReadAuctionDto 경매_정보_dto2 = new ReadAuctionDto( + 2L, + "경매 상품 2", + "이것은 경매 상품 2 입니다.", + 1_000, + 1_000, + null, + false, + LocalDateTime.now(), + LocalDateTime.now(), + List.of(직거래_지역_정보_dto), + List.of(1L), + 2, + "main2", + "sub2", + 1L, + 1L, + "판매자", + 3.5d, + false, + AuctionStatus.UNBIDDEN + ); + protected ReadAuctionsDto 사용자의_경매들_정보_dto = new ReadAuctionsDto(List.of(경매_정보_dto2, 경매_정보_dto1), true); + protected ReadAuctionsDto 사용자가_참여한_경매들_정보_dto = new ReadAuctionsDto(List.of(경매_정보_dto2, 경매_정보_dto1), true); +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/fixture/UserControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/fixture/UserControllerFixture.java new file mode 100644 index 000000000..7d63d3ad5 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/fixture/UserControllerFixture.java @@ -0,0 +1,45 @@ +package com.ddang.ddang.user.presentation.fixture; + +import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims; +import com.ddang.ddang.configuration.CommonControllerSliceTest; +import com.ddang.ddang.user.application.dto.ReadUserDto; +import com.ddang.ddang.user.presentation.dto.request.UpdateUserRequest; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; + +import java.nio.charset.StandardCharsets; + +@SuppressWarnings("NonAsciiCharacters") +public class UserControllerFixture extends CommonControllerSliceTest { + + protected String 액세스_토큰_값 = "Bearer accessToken"; + protected PrivateClaims 사용자_ID_클레임 = new PrivateClaims(1L); + protected String 탈퇴한_사용자_이름 = "알 수 없음"; + + protected ReadUserDto 사용자_정보_조회_dto = new ReadUserDto(1L, "사용자1", 1L, 4.6d, "12345", false); + protected ReadUserDto 탈퇴한_사용자_정보_조회_dto = new ReadUserDto(1L, "사용자1", 1L, 4.6d, "12345", true); + protected UpdateUserRequest 수정할_이름_request = new UpdateUserRequest("updateName"); + protected ReadUserDto 수정후_사용자_정보_조회_dto = new ReadUserDto(1L, 수정할_이름_request.name(), 1L, 4.6d, "12345", false); + private String json = "{\"name\":\"" + 수정할_이름_request.name() + "\"}"; + private byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8); + protected MockMultipartFile 수정할_이름 = new MockMultipartFile( + "request", + "request", + MediaType.APPLICATION_JSON_VALUE, + jsonBytes + ); + protected MockMultipartFile 수정할_프로필_이미지 = new MockMultipartFile( + "profileImage", + "image.png", + MediaType.IMAGE_PNG_VALUE, + new byte[]{1} + ); + protected MockMultipartFile 프로필_이미지가_없는_경우_파일 = new MockMultipartFile( + "profileImage", + (byte[]) null + ); + protected MockMultipartFile 이름을_수정하지_않는_경우 = new MockMultipartFile( + "request", + (byte[]) null + ); +} diff --git a/backend/ddang/src/test/resources/application.yml b/backend/ddang/src/test/resources/application.yml index 95182968c..1b0aeb253 100644 --- a/backend/ddang/src/test/resources/application.yml +++ b/backend/ddang/src/test/resources/application.yml @@ -1,4 +1,7 @@ spring: + profiles: + default: test + datasource: url: jdbc:h2:mem:testdb username: sa @@ -40,3 +43,9 @@ oauth2: providers: kakao: user-info-uri: https://kapi.kakao.com/v2/user/me + user-unlink-uri: https://kapi.kakao.com/v1/user/unlink + +fcm: + enabled: false + key: + path: firebase/private-key.json