diff --git a/.github/workflows/be-cd_dev-docker.yml b/.github/workflows/be-cd_dev-docker.yml index bef7adaea..8de997a71 100644 --- a/.github/workflows/be-cd_dev-docker.yml +++ b/.github/workflows/be-cd_dev-docker.yml @@ -117,16 +117,24 @@ jobs: # Security settings JWT_TOKEN_SECRET_KEY=${{ secrets.JWT_TOKEN_SECRET_KEY }} JWT_TOKEN_EXPIRE_CYCLE=${{ secrets.JWT_TOKEN_EXPIRE_CYCLE }} + ACCESS_TOKEN_EXPIRE_CYCLE=${{ secrets.ACCESS_TOKEN_EXPIRE_CYCLE }} + REFRESH_TOKEN_EXPIRE_CYCLE=${{ secrets.REFRESH_TOKEN_EXPIRE_CYCLE }} JWT_SIGN_ALGORITHM=${{ secrets.JWT_SIGN_ALGORITHM }} # Cookie settings COOKIE_ACCESS_TOKEN_KEY=${{ secrets.COOKIE_ACCESS_TOKEN_KEY }} + COOKIE_REFRESH_TOKEN_KEY=${{ secrets.COOKIE_REFRESH_TOKEN_KEY }} COOKIE_HTTP_ONLY=${{ secrets.COOKIE_HTTP_ONLY }} COOKIE_SECURE=${{ secrets.COOKIE_SECURE }} COOKIE_DOMAIN=${{ secrets.COOKIE_DOMAIN }} COOKIE_PATH=${{ secrets.COOKIE_PATH }} COOKIE_SAME_SITE=${{ secrets.COOKIE_SAME_SITE }} COOKIE_MAX_AGE=${{ secrets.COOKIE_MAX_AGE }} + + # Redis + REDIS_PORT=${{ secrets.REDIS_PORT }} + REDIS_HOST=${{ secrets.REDIS_HOST }} + REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }} EOF # - name: Check if MySQL container is running diff --git a/.github/workflows/be-cd_prod-docker.yml b/.github/workflows/be-cd_prod-docker.yml index c9e118a11..78cd332c9 100644 --- a/.github/workflows/be-cd_prod-docker.yml +++ b/.github/workflows/be-cd_prod-docker.yml @@ -2,8 +2,13 @@ name: BE/CD - [PROD] Build & Deploy on: workflow_dispatch: - push: - branches: be/release + inputs: + release-ver: + description: 'Release version tag (ex. v0.0.1)' + required: true + rollback-ver: + description: 'Rollback version tag (ex. v0.0.0)' + required: true jobs: build: @@ -42,15 +47,10 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Get current date and time - id: datetime - run: | - echo "datetime=$(date +'%Y%m%d%H%M%S')" >> "$GITHUB_OUTPUT" - - name: Image build and push run: | - docker build --build-arg PROFILE=prod -t ${{ secrets.DOCKER_REPO_NAME }}/cruru:prod-${{ steps.datetime.outputs.datetime }} --platform linux/arm64 . - docker push ${{ secrets.DOCKER_REPO_NAME }}/cruru:prod-${{ steps.datetime.outputs.datetime }} + docker build --build-arg PROFILE=prod -t ${{ secrets.DOCKER_REPO_NAME }}/cruru:${{ inputs.release-ver }} --platform linux/arm64 . + docker push ${{ secrets.DOCKER_REPO_NAME }}/cruru:${{ inputs.release-ver }} - name: Upload docker-compose yaml script to artifact uses: actions/upload-artifact@v4 @@ -59,8 +59,6 @@ jobs: path: | ${{ github.workspace }}/backend/docker-compose.prod.yml ${{ github.workspace }}/backend/promtail-config.yml - outputs: - BUILD_VERSION: ${{ steps.datetime.outputs.datetime }} deploy: environment: prod @@ -76,6 +74,82 @@ jobs: run: working-directory: backend steps: + + - name: Set reviewer and sender variables + run: | + ASSIGNEE_LOGIN=${{ github.event.sender }} + echo "SENDER_SLACK_ID=${ASSIGNEE_LOGIN@L}" >> ${GITHUB_ENV} + + - name: 배포시작 알림 + uses: slackapi/slack-github-action@v1.26.0 + with: + channel-id: C07S8K0PHTQ + payload: | + { + "blocks": [ + { + "type": "divider" + }, + { + "type": "header", + "text": { + "type": "plain_text", + "text": "🚀 운영 배포 시작 🚀", + "emoji": true + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*배포자:*\n <@${{ env.SENDER_SLACK_ID }}>" + } + ] + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "🚀 *배포 버전:*\n${{ inputs.release-ver }}" + } + ] + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "🚀 *롤백 버전:*\n${{ inputs.rollback-ver }}" + } + ] + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "🕹️ Action 실행 현황 🕹️", + "emoji": true + }, + "value": "PR_LINK", + "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", + "action_id": "actionId-1" + } + ] + }, + { + "type": "divider" + } + ] + } + + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + - name: Set docker-compose YAML script to runner uses: actions/download-artifact@v4 with: @@ -87,7 +161,7 @@ jobs: cat < .env # Docker Hub info from Github Secrets DOCKER_REPO_NAME=${{ secrets.DOCKER_REPO_NAME }} - DOCKER_IMAGE_VERSION_TAG=prod-${{ needs.build.outputs.BUILD_VERSION }} + DOCKER_IMAGE_VERSION_TAG=${{ inputs.release-ver }} # DB Configuration secrets info from Github Secrets DB_PORT=${{ secrets.DB_PORT }} @@ -100,14 +174,15 @@ jobs: # DB server configuration secrets info from Github Secrets APP_IP_ADDRESS=${{ secrets.APP_IP_ADDRESS }} - SERVER_BINDING_PORT=${{ secrets.SERVER_BINDING_PORT }} - SERVER_PORT=${{ secrets.SERVER_PORT }} - SUBNET=${{ secrets.SUBNET }} + BLUE_SERVER_BINDING_PORT=${{ secrets.BLUE_SERVER_BINDING_PORT }} + GREEN_SERVER_BINDING_PORT=${{ secrets.GREEN_SERVER_BINDING_PORT }} + BLUE_SERVER_PORT=${{ secrets.BLUE_SERVER_PORT }} + GREEN_SERVER_PORT=${{ secrets.GREEN_SERVER_PORT }} # Monitoring configuration server info from Github secrets MONITORING_INSTANCE_ADDR_LOKI_PORT=${{ secrets.MONITORING_INSTANCE_ADDR_LOKI_PORT }} - MONITORING_BINDING_PORT=${{ secrets.MONITORING_BINDING_PORT }} - MONITORING_PORT=${{ secrets.MONITORING_PORT }} + BLUE_MONITORING_BINDING_PORT=${{ secrets.BLUE_MONITORING_BINDING_PORT }} + GREEN_MONITORING_BINDING_PORT=${{ secrets.GREEN_MONITORING_BINDING_PORT }} MONITORING_BASE_PATH=${{ secrets.MONITORING_BASE_PATH }} MONITORING_PROFILE=${{ secrets.MONITORING_PROFILE }} @@ -121,25 +196,171 @@ jobs: # Security settings JWT_TOKEN_SECRET_KEY=${{ secrets.JWT_TOKEN_SECRET_KEY }} JWT_TOKEN_EXPIRE_CYCLE=${{ secrets.JWT_TOKEN_EXPIRE_CYCLE }} + ACCESS_TOKEN_EXPIRE_CYCLE=${{ secrets.ACCESS_TOKEN_EXPIRE_CYCLE }} + REFRESH_TOKEN_EXPIRE_CYCLE=${{ secrets.REFRESH_TOKEN_EXPIRE_CYCLE }} JWT_SIGN_ALGORITHM=${{ secrets.JWT_SIGN_ALGORITHM }} # Cookie settings COOKIE_ACCESS_TOKEN_KEY=${{ secrets.COOKIE_ACCESS_TOKEN_KEY }} + COOKIE_REFRESH_TOKEN_KEY=${{ secrets.COOKIE_REFRESH_TOKEN_KEY }} COOKIE_HTTP_ONLY=${{ secrets.COOKIE_HTTP_ONLY }} COOKIE_SECURE=${{ secrets.COOKIE_SECURE }} COOKIE_DOMAIN=${{ secrets.COOKIE_DOMAIN }} COOKIE_PATH=${{ secrets.COOKIE_PATH }} COOKIE_SAME_SITE=${{ secrets.COOKIE_SAME_SITE }} - COOKIE_MAX_AGE=${{ secrets.COOKIE_MAX_AGE }} + COOKIE_MAX_AGE=${{ secrets.COOKIE_MAX_AGE }} + + # Redis + REDIS_PORT=${{ secrets.REDIS_PORT }} + REDIS_HOST=${{ secrets.REDIS_HOST }} + REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }} EOF - - name: Stop and remove existing containers + - name: Run zero down time Deploy Script run: | - sudo docker-compose -f docker-compose.prod.yml down --rmi all + chmod +x ../deploy.sh + ../deploy.sh - - name: Deploy docker container - run: | - sudo docker-compose --env-file .env -f docker-compose.prod.yml up -d + - name: 배포시작 알림 + uses: slackapi/slack-github-action@v1.26.0 + with: + channel-id: C07S8K0PHTQ + payload: | + { + "blocks": [ + { + "type": "divider" + }, + { + "type": "header", + "text": { + "type": "plain_text", + "text": "👍🏻 운영 배포 완료 👍🏻", + "emoji": true + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "🚀 *배포 버전:*\n${{ inputs.release-ver }}" + } + ] + }, + { + "type": "divider" + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + + roll-back-on-fail: + environment: prod + strategy: + matrix: + runners: [be-prod-a, be-prod-b] + + runs-on: [self-hosted, '${{ matrix.runners }}'] + needs: deploy + if: failure() + + defaults: + run: + working-directory: backend + steps: + - name: Extract secrets as .env file + run: | + cat < .env + # Docker Hub info from Github Secrets + DOCKER_REPO_NAME=${{ secrets.DOCKER_REPO_NAME }} + DOCKER_IMAGE_VERSION_TAG=${{ inputs.rollback-ver }} + + # DB Configuration secrets info from Github Secrets + DB_PORT=${{ secrets.DB_PORT }} + DB_IP_ADDRESS=${{ secrets.DB_IP_ADDRESS }} + READ_DB_URL=${{ secrets.READ_DB_URL }} + WRITE_DB_URL=${{ secrets.WRITE_DB_URL }} + DB_USER=${{ secrets.DB_USER }} + DB_PASSWORD=${{ secrets.DB_PASSWORD }} + DDL_AUTO=${{ secrets.DDL_AUTO }} + + # DB server configuration secrets info from Github Secrets + APP_IP_ADDRESS=${{ secrets.APP_IP_ADDRESS }} + BLUE_SERVER_BINDING_PORT=${{ secrets.BLUE_SERVER_BINDING_PORT }} + GREEN_SERVER_BINDING_PORT=${{ secrets.GREEN_SERVER_BINDING_PORT }} + BLUE_SERVER_PORT=${{ secrets.BLUE_SERVER_PORT }} + GREEN_SERVER_PORT=${{ secrets.GREEN_SERVER_PORT }} + + # Monitoring configuration server info from Github secrets + MONITORING_INSTANCE_ADDR_LOKI_PORT=${{ secrets.MONITORING_INSTANCE_ADDR_LOKI_PORT }} + BLUE_MONITORING_BINDING_PORT=${{ secrets.BLUE_MONITORING_BINDING_PORT }} + GREEN_MONITORING_BINDING_PORT=${{ secrets.GREEN_MONITORING_BINDING_PORT }} + MONITORING_BASE_PATH=${{ secrets.MONITORING_BASE_PATH }} + MONITORING_PROFILE=${{ secrets.MONITORING_PROFILE }} + + # Apply configuration server info from Github secrets + APPLY_POST_BASE_URL=${{ secrets.APPLY_POST_BASE_URL }} + + # Email Auth info + EMAIL_USERNAME=${{ secrets.EMAIL_USERNAME }} + EMAIL_PASSWORD=${{ secrets.EMAIL_PASSWORD }} + + # Security settings + JWT_TOKEN_SECRET_KEY=${{ secrets.JWT_TOKEN_SECRET_KEY }} + JWT_TOKEN_EXPIRE_CYCLE=${{ secrets.JWT_TOKEN_EXPIRE_CYCLE }} + ACCESS_TOKEN_EXPIRE_CYCLE=${{ secrets.ACCESS_TOKEN_EXPIRE_CYCLE }} + REFRESH_TOKEN_EXPIRE_CYCLE=${{ secrets.REFRESH_TOKEN_EXPIRE_CYCLE }} + JWT_SIGN_ALGORITHM=${{ secrets.JWT_SIGN_ALGORITHM }} + + # Cookie settings + COOKIE_ACCESS_TOKEN_KEY=${{ secrets.COOKIE_ACCESS_TOKEN_KEY }} + COOKIE_REFRESH_TOKEN_KEY=${{ secrets.COOKIE_REFRESH_TOKEN_KEY }} + COOKIE_HTTP_ONLY=${{ secrets.COOKIE_HTTP_ONLY }} + COOKIE_SECURE=${{ secrets.COOKIE_SECURE }} + COOKIE_DOMAIN=${{ secrets.COOKIE_DOMAIN }} + COOKIE_PATH=${{ secrets.COOKIE_PATH }} + COOKIE_SAME_SITE=${{ secrets.COOKIE_SAME_SITE }} + COOKIE_MAX_AGE=${{ secrets.COOKIE_MAX_AGE }} + EOF + + - name: Run zero down time Deploy Script + run: | + chmod +x ../deploy.sh + ../deploy.sh - - name: Run server status check script with timeout - run: ../check_server_status_with_timeout.sh + - name: 배포시작 알림 + uses: slackapi/slack-github-action@v1.26.0 + with: + channel-id: C07S8K0PHTQ + payload: | + { + "blocks": [ + { + "type": "divider" + }, + { + "type": "header", + "text": { + "type": "plain_text", + "text": "❌ 운영 배포 실패 및 롤백 ❌", + "emoji": true + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "🚀 *롤백 버전:*\n${{ inputs.rollback-ver }}" + } + ] + }, + { + "type": "divider" + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} diff --git a/.github/workflows/be-cd_test-docker.yml b/.github/workflows/be-cd_test-docker.yml index 2f21acebb..6eaa811a3 100644 --- a/.github/workflows/be-cd_test-docker.yml +++ b/.github/workflows/be-cd_test-docker.yml @@ -112,16 +112,24 @@ jobs: # Security settings JWT_TOKEN_SECRET_KEY=${{ secrets.JWT_TOKEN_SECRET_KEY }} JWT_TOKEN_EXPIRE_CYCLE=${{ secrets.JWT_TOKEN_EXPIRE_CYCLE }} + ACCESS_TOKEN_EXPIRE_CYCLE=${{ secrets.ACCESS_TOKEN_EXPIRE_CYCLE }} + REFRESH_TOKEN_EXPIRE_CYCLE=${{ secrets.REFRESH_TOKEN_EXPIRE_CYCLE }} JWT_SIGN_ALGORITHM=${{ secrets.JWT_SIGN_ALGORITHM }} # Cookie settings COOKIE_ACCESS_TOKEN_KEY=${{ secrets.COOKIE_ACCESS_TOKEN_KEY }} + COOKIE_REFRESH_TOKEN_KEY=${{ secrets.COOKIE_REFRESH_TOKEN_KEY }} COOKIE_HTTP_ONLY=${{ secrets.COOKIE_HTTP_ONLY }} COOKIE_SECURE=${{ secrets.COOKIE_SECURE }} COOKIE_DOMAIN=${{ secrets.COOKIE_DOMAIN }} COOKIE_PATH=${{ secrets.COOKIE_PATH }} COOKIE_SAME_SITE=${{ secrets.COOKIE_SAME_SITE }} - COOKIE_MAX_AGE=${{ secrets.COOKIE_MAX_AGE }} + COOKIE_MAX_AGE=${{ secrets.COOKIE_MAX_AGE }} + + # Redis + REDIS_PORT=${{ secrets.REDIS_PORT }} + REDIS_HOST=${{ secrets.REDIS_HOST }} + REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }} EOF - name: Stop and remove existing containers diff --git a/.github/workflows/be-release-tag-create.yml b/.github/workflows/be-release-tag-create.yml new file mode 100644 index 000000000..06545c721 --- /dev/null +++ b/.github/workflows/be-release-tag-create.yml @@ -0,0 +1,63 @@ +name: BE/CD - [PROD] Build & Deploy + +on: + push: + branches: be/release + +jobs: + check-commit: + runs-on: ubuntu-latest + outputs: + result: ${{ steps.check.outputs.result }} + version: ${{ steps.get-version.outputs.version }} + steps: + - uses: actions/checkout@v3 + - name: Check commit message + id: check + run: echo "result=$(echo '${{ github.event.head_commit.message }}' | grep -oP '^Release v(\d|\.)+$')" >> $GITHUB_OUTPUT + shell: bash + - name: Get version + id: get-version + run: echo "version=$(echo '${{ github.event.head_commit.message }}' | grep -oP 'v(\d|\.)+')" >> $GITHUB_OUTPUT + shell: bash + create-tag: + runs-on: ubuntu-latest + needs: [ "check-commit" ] + if: ${{ needs.check-commit.outputs.result != '' }} + outputs: + tag-exists: ${{ steps.create-tag.outputs.tag_exists }} + release-body: ${{ steps.generate-body.outputs.body }} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Generate body + id: generate-body + run: | + EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) + git_logs=$(git log "$(git describe --tags --abbrev=0)"..HEAD) + git_logs="${git_logs//$'\n'/$'\n'- }" + { + echo "body<<$EOF" + echo "- $git_logs" + echo "$EOF" + } >>"$GITHUB_OUTPUT" + shell: bash + - uses: rickstaa/action-create-tag@v1 + id: create-tag + with: + tag: ${{ needs.check-commit.outputs.version }} + tag_exists_error: true + message: ${{ needs.check-commit.outputs.version }} + create-release: + runs-on: ubuntu-latest + needs: [ "check-commit", "create-tag" ] + if: ${{ needs.create-tag.outputs.tag-exists == 'false' }} + steps: + - uses: actions/checkout@v3 + - name: Create a GitHub release + uses: ncipollo/release-action@v1 + with: + tag: ${{ needs.check-commit.outputs.version }} + name: ${{ needs.check-commit.outputs.version }} + body: ${{ needs.create-tag.outputs.release-body }} diff --git a/backend/build.gradle b/backend/build.gradle index 5a9b4a0fe..c5a1b2bb3 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -41,6 +41,9 @@ dependencies { implementation 'org.flywaydb:flyway-core:9.22.3' implementation 'org.flywaydb:flyway-mysql' + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + // Lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/backend/docker-compose.local.yml b/backend/docker-compose.local.yml new file mode 100644 index 000000000..7c907fcc6 --- /dev/null +++ b/backend/docker-compose.local.yml @@ -0,0 +1,17 @@ +version: '3.8' + +services: + redis: + image: redis:latest + container_name: redis-local + env_file: + - .env + command: "redis-server --requirepass ${LOCAL_REDIS_PASSWORD}" + ports: + - "${LOCAL_REDIS_PORT}" + networks: + - redis_network + +networks: + redis_network: + driver: bridge diff --git a/backend/docker-compose.prod.yml b/backend/docker-compose.prod.yml index 59206e011..a9ce5cd69 100644 --- a/backend/docker-compose.prod.yml +++ b/backend/docker-compose.prod.yml @@ -1,24 +1,54 @@ version: '3.8' - services: - application: - container_name: app_container + app_container-blue: + container_name: app_container-blue platform: linux/arm64 restart: always - image: ${DOCKER_REPO_NAME}/cruru:${DOCKER_IMAGE_VERSION_TAG} + image: cruru/cruru:${DOCKER_IMAGE_VERSION_TAG} ports: - - ${SERVER_BINDING_PORT} - - ${MONITORING_BINDING_PORT} + - ${BLUE_SERVER_BINDING_PORT} + - ${BLUE_MONITORING_BINDING_PORT} env_file: - .env environment: - PROFILE: prod + SERVER_PORT: ${BLUE_SERVER_PORT} + PROFILE: test TZ: Asia/Seoul volumes: - "./log:/log" networks: - cruru_network: - ipv4_address: ${APP_IP_ADDRESS} + - cruru_network + + app_container-green: + container_name: app_container-green + platform: linux/arm64 + restart: always + image: cruru/cruru:${DOCKER_IMAGE_VERSION_TAG} + ports: + - ${GREEN_SERVER_BINDING_PORT} + - ${GREEN_MONITORING_BINDING_PORT} + env_file: + - .env + environment: + SERVER_PORT: ${GREEN_SERVER_PORT} + PROFILE: test + TZ: Asia/Seoul + volumes: + - "./log:/log" + networks: + - cruru_network + + nginx_container: + image: nginx:latest + container_name: nginx_container + restart: always + ports: + - "80:80" + volumes: + - ./nginx-conf:/etc/nginx/conf.d + - ./html:/usr/share/nginx/html + networks: + - cruru_network promtail: environment: @@ -32,12 +62,8 @@ services: - "./log:/log" command: -config.expand-env=true -config.file=/etc/promtail/config.yml networks: - cruru_network: - ipv4_address: 172.18.0.4 + - cruru_network networks: cruru_network: - driver: bridge - ipam: - config: - - subnet: ${SUBNET} + external: true diff --git a/backend/src/main/java/com/cruru/applicant/domain/ApplicantSortOption.java b/backend/src/main/java/com/cruru/applicant/domain/ApplicantSortOption.java index d70331d63..173ebf28e 100644 --- a/backend/src/main/java/com/cruru/applicant/domain/ApplicantSortOption.java +++ b/backend/src/main/java/com/cruru/applicant/domain/ApplicantSortOption.java @@ -1,9 +1,9 @@ package com.cruru.applicant.domain; import com.cruru.applicant.domain.dto.ApplicantCard; -import com.cruru.applicant.exception.badrequest.ApplicantSortException; import java.util.Arrays; import java.util.Comparator; +import java.util.Optional; public enum ApplicantSortOption { @@ -16,19 +16,26 @@ public enum ApplicantSortOption { this.comparator = comparator; } - public static Comparator getCombinedComparator(String sortByCreatedAt, String sortByScore) { - ApplicantSortOption createdAtOption = convertToSortOption(sortByCreatedAt); - ApplicantSortOption scoreOption = convertToSortOption(sortByScore); - - return createdAtOption.getCreatedAtComparator() - .thenComparing(scoreOption.getScoreComparator()); + public static Comparator getComparator(String sortByCreatedAt, String sortByScore) { + Optional createdAtOption = convertToSortOption(sortByCreatedAt); + Optional scoreOption = convertToSortOption(sortByScore); + + if (createdAtOption.isPresent()) { + return createdAtOption.get().getCreatedAtComparator(); + } + if (scoreOption.isPresent()) { + return scoreOption.get().getScoreComparator(); + } + return Comparator.comparing(ApplicantCard::createdAt, DESC.comparator); } - private static ApplicantSortOption convertToSortOption(String sortOption) { + private static Optional convertToSortOption(String sortOption) { + if (sortOption == null || sortOption.isEmpty()) { + return Optional.empty(); + } return Arrays.stream(ApplicantSortOption.values()) .filter(option -> option.name().equalsIgnoreCase(sortOption)) - .findAny() - .orElseThrow(ApplicantSortException::new); + .findAny(); } private Comparator getCreatedAtComparator() { diff --git a/backend/src/main/java/com/cruru/applicant/exception/badrequest/ApplicantSortException.java b/backend/src/main/java/com/cruru/applicant/exception/badrequest/ApplicantSortException.java deleted file mode 100644 index 6093875cc..000000000 --- a/backend/src/main/java/com/cruru/applicant/exception/badrequest/ApplicantSortException.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.cruru.applicant.exception.badrequest; - -import com.cruru.advice.badrequest.BadRequestException; - -public class ApplicantSortException extends BadRequestException { - - private static final String MESSAGE = "지원하는 정렬 조건이 아닙니다."; - - public ApplicantSortException() { - super(MESSAGE); - } -} diff --git a/backend/src/main/java/com/cruru/applicant/service/ApplicantService.java b/backend/src/main/java/com/cruru/applicant/service/ApplicantService.java index 6a97e412a..7a01e36df 100644 --- a/backend/src/main/java/com/cruru/applicant/service/ApplicantService.java +++ b/backend/src/main/java/com/cruru/applicant/service/ApplicantService.java @@ -128,7 +128,7 @@ public List findApplicantCards( return applicantCards.stream() .filter(card -> filterByScore(card, minScore, maxScore)) .filter(card -> EvaluationStatus.matches(card, evaluationStatus)) - .sorted(ApplicantSortOption.getCombinedComparator(sortByCreatedAt, sortByScore)) + .sorted(ApplicantSortOption.getComparator(sortByCreatedAt, sortByScore)) .toList(); } diff --git a/backend/src/main/java/com/cruru/auth/controller/AuthController.java b/backend/src/main/java/com/cruru/auth/controller/AuthController.java index 15a7ab3a1..c75da79c3 100644 --- a/backend/src/main/java/com/cruru/auth/controller/AuthController.java +++ b/backend/src/main/java/com/cruru/auth/controller/AuthController.java @@ -2,6 +2,7 @@ import com.cruru.auth.controller.request.LoginRequest; import com.cruru.auth.controller.response.LoginResponse; +import com.cruru.auth.controller.response.TokenResponse; import com.cruru.auth.facade.AuthFacade; import com.cruru.club.facade.ClubFacade; import com.cruru.global.util.CookieManager; @@ -26,19 +27,23 @@ public class AuthController { @PostMapping("/login") public ResponseEntity login(@RequestBody @Valid LoginRequest request) { - String token = authFacade.login(request); + TokenResponse tokenResponse = authFacade.login(request); long clubId = clubFacade.findByMemberEmail(request.email()); - ResponseCookie cookie = cookieManager.createTokenCookie(token); + ResponseCookie accessTokenCookie = cookieManager.createAccessTokenCookie(tokenResponse.accessToken()); + ResponseCookie refreshTokenCookie = cookieManager.createRefreshTokenCookie(tokenResponse.refreshToken()); return ResponseEntity.ok() - .header(HttpHeaders.SET_COOKIE, cookie.toString()) + .header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()) + .header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()) .body(new LoginResponse(clubId)); } @PostMapping("/logout") public ResponseEntity logout() { - ResponseCookie cookie = cookieManager.clearTokenCookie(); + ResponseCookie accessTokenCookie = cookieManager.clearAccessTokenCookie(); + ResponseCookie refreshTokenCookie = cookieManager.clearRefreshTokenCookie(); return ResponseEntity.noContent() - .header(HttpHeaders.SET_COOKIE, cookie.toString()) + .header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()) + .header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()) .build(); } } diff --git a/backend/src/main/java/com/cruru/auth/controller/response/TokenResponse.java b/backend/src/main/java/com/cruru/auth/controller/response/TokenResponse.java new file mode 100644 index 000000000..609ee3ea3 --- /dev/null +++ b/backend/src/main/java/com/cruru/auth/controller/response/TokenResponse.java @@ -0,0 +1,5 @@ +package com.cruru.auth.controller.response; + +public record TokenResponse(String accessToken, String refreshToken) { + +} diff --git a/backend/src/main/java/com/cruru/auth/domain/AccessToken.java b/backend/src/main/java/com/cruru/auth/domain/AccessToken.java new file mode 100644 index 000000000..51c45b404 --- /dev/null +++ b/backend/src/main/java/com/cruru/auth/domain/AccessToken.java @@ -0,0 +1,18 @@ +package com.cruru.auth.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class AccessToken implements Token { + + private final String token; + + @Override + public String toString() { + return "AccessToken{" + + "token='" + token + '\'' + + '}'; + } +} diff --git a/backend/src/main/java/com/cruru/auth/domain/RefreshToken.java b/backend/src/main/java/com/cruru/auth/domain/RefreshToken.java new file mode 100644 index 000000000..d8aa5ebeb --- /dev/null +++ b/backend/src/main/java/com/cruru/auth/domain/RefreshToken.java @@ -0,0 +1,69 @@ +package com.cruru.auth.domain; + +import com.cruru.BaseEntity; +import com.cruru.member.domain.Member; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import java.util.Objects; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter +public class RefreshToken extends BaseEntity implements Token { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "refresh_token_id") + private Long id; + + @Column(nullable = false, unique = true) + private String token; + + @OneToOne + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + public RefreshToken(String token, Member member) { + this(null, token, member); + } + + public boolean isSameToken(String token) { + return this.token.equals(token); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RefreshToken that = (RefreshToken) o; + return Objects.equals(getId(), that.getId()); + } + + @Override + public int hashCode() { + return Objects.hashCode(getId()); + } + + @Override + public String toString() { + return "RefreshToken{" + + "id=" + id + + ", token='" + token + '\'' + + ", member=" + member + + '}'; + } +} diff --git a/backend/src/main/java/com/cruru/auth/domain/Token.java b/backend/src/main/java/com/cruru/auth/domain/Token.java new file mode 100644 index 000000000..31a5f6f5b --- /dev/null +++ b/backend/src/main/java/com/cruru/auth/domain/Token.java @@ -0,0 +1,6 @@ +package com.cruru.auth.domain; + +public interface Token { + + String getToken(); +} diff --git a/backend/src/main/java/com/cruru/auth/domain/repository/RefreshTokenRepository.java b/backend/src/main/java/com/cruru/auth/domain/repository/RefreshTokenRepository.java new file mode 100644 index 000000000..d59099465 --- /dev/null +++ b/backend/src/main/java/com/cruru/auth/domain/repository/RefreshTokenRepository.java @@ -0,0 +1,22 @@ +package com.cruru.auth.domain.repository; + +import com.cruru.auth.domain.RefreshToken; +import com.cruru.member.domain.Member; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface RefreshTokenRepository extends JpaRepository { + + boolean existsByToken(String token); + + boolean existsByMember(Member member); + + Optional findByMember(Member member); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE RefreshToken rt SET rt.token = :refreshToken WHERE rt.member = :member") + void updateRefreshTokenByMember(@Param("refreshToken") String refreshToken, @Param("member") Member member); +} diff --git a/backend/src/main/java/com/cruru/auth/facade/AuthFacade.java b/backend/src/main/java/com/cruru/auth/facade/AuthFacade.java index 1d96825dc..1c9c1cab6 100644 --- a/backend/src/main/java/com/cruru/auth/facade/AuthFacade.java +++ b/backend/src/main/java/com/cruru/auth/facade/AuthFacade.java @@ -1,6 +1,8 @@ package com.cruru.auth.facade; import com.cruru.auth.controller.request.LoginRequest; +import com.cruru.auth.controller.response.TokenResponse; +import com.cruru.auth.domain.Token; import com.cruru.auth.exception.LoginFailedException; import com.cruru.auth.service.AuthService; import com.cruru.member.domain.Member; @@ -17,11 +19,19 @@ public class AuthFacade { private final AuthService authService; private final MemberService memberService; - public String login(LoginRequest request) { + @Transactional + public TokenResponse login(LoginRequest request) { Member member = memberService.findByEmail(request.email()); if (authService.isNotVerifiedPassword(request.password(), member.getPassword())) { throw new LoginFailedException(); } - return authService.createToken(member); + + return createTokens(member); + } + + private TokenResponse createTokens(Member member) { + Token accessToken = authService.createAccessToken(member); + Token refreshToken = authService.createRefreshToken(member); + return new TokenResponse(accessToken.getToken(), refreshToken.getToken()); } } diff --git a/backend/src/main/java/com/cruru/auth/security/TokenProperties.java b/backend/src/main/java/com/cruru/auth/security/TokenProperties.java index e96192294..fbd674d3e 100644 --- a/backend/src/main/java/com/cruru/auth/security/TokenProperties.java +++ b/backend/src/main/java/com/cruru/auth/security/TokenProperties.java @@ -3,6 +3,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties("security.jwt.token") -public record TokenProperties(String secretKey, Long expireLength, String algorithm) { +public record TokenProperties(String secretKey, Long accessExpireLength, Long refreshExpireLength, String algorithm) { } diff --git a/backend/src/main/java/com/cruru/auth/security/TokenProvider.java b/backend/src/main/java/com/cruru/auth/security/TokenProvider.java index 49ab720c0..5ffd6ed49 100644 --- a/backend/src/main/java/com/cruru/auth/security/TokenProvider.java +++ b/backend/src/main/java/com/cruru/auth/security/TokenProvider.java @@ -5,9 +5,11 @@ public interface TokenProvider { - String createToken(Map claims); + String createToken(Map claims, Long expireLength); - boolean isAlive(String token) throws IllegalTokenException; + boolean isSignatureValid(String token) throws IllegalTokenException; + + boolean isTokenExpired(String token) throws IllegalTokenException; String extractClaim(String token, String key) throws IllegalTokenException; } diff --git a/backend/src/main/java/com/cruru/auth/security/jwt/JwtTokenProvider.java b/backend/src/main/java/com/cruru/auth/security/jwt/JwtTokenProvider.java index 975bb9474..72e08a4ca 100644 --- a/backend/src/main/java/com/cruru/auth/security/jwt/JwtTokenProvider.java +++ b/backend/src/main/java/com/cruru/auth/security/jwt/JwtTokenProvider.java @@ -5,8 +5,8 @@ import com.cruru.auth.security.TokenProvider; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; import java.util.Map; @@ -20,36 +20,52 @@ public class JwtTokenProvider implements TokenProvider { private final TokenProperties tokenProperties; @Override - public String createToken(Map claims) { + public String createToken(Map claims, Long expireLength) { Date now = new Date(); - Date validity = new Date(now.getTime() + tokenProperties.expireLength()); + Date validity = new Date(now.getTime() + expireLength); return Jwts.builder() .addClaims(claims) .setIssuedAt(now) .setExpiration(validity) .signWith(SignatureAlgorithm.valueOf(tokenProperties.algorithm()), - tokenProperties.secretKey().getBytes()) + tokenProperties.secretKey()) .compact(); } @Override - public boolean isAlive(String token) throws IllegalTokenException { - Claims claims = extractClaims(token); - Date expiration = claims.getExpiration(); - Date now = new Date(); - return expiration.after(now); + public boolean isSignatureValid(String token) { + try { + extractClaims(token); + return true; + } catch (ExpiredJwtException e) { + return true; + } catch (IllegalTokenException e) { + return false; + } + } + + @Override + public boolean isTokenExpired(String token) throws IllegalTokenException { + try { + Claims claims = extractClaims(token); + Date expiration = claims.getExpiration(); + Date now = new Date(); + return !expiration.after(now); + } catch (ExpiredJwtException e) { + return true; + } catch (IllegalTokenException e) { + return false; + } } private Claims extractClaims(String token) { try { return Jwts.parser() - .setSigningKey(tokenProperties.secretKey().getBytes()) + .setSigningKey(tokenProperties.secretKey()) .parseClaimsJws(token) .getBody(); - } catch (ExpiredJwtException e) { - return e.getClaims(); - } catch (JwtException | IllegalArgumentException e) { + } catch (MalformedJwtException | IllegalArgumentException e) { throw new IllegalTokenException(); } } diff --git a/backend/src/main/java/com/cruru/auth/service/AuthService.java b/backend/src/main/java/com/cruru/auth/service/AuthService.java index a777b467c..f0b617a90 100644 --- a/backend/src/main/java/com/cruru/auth/service/AuthService.java +++ b/backend/src/main/java/com/cruru/auth/service/AuthService.java @@ -1,9 +1,19 @@ package com.cruru.auth.service; +import com.cruru.auth.controller.response.TokenResponse; +import com.cruru.auth.domain.AccessToken; +import com.cruru.auth.domain.RefreshToken; +import com.cruru.auth.domain.Token; +import com.cruru.auth.domain.repository.RefreshTokenRepository; import com.cruru.auth.exception.IllegalTokenException; +import com.cruru.auth.exception.LoginExpiredException; import com.cruru.auth.security.PasswordValidator; +import com.cruru.auth.security.TokenProperties; import com.cruru.auth.security.TokenProvider; import com.cruru.member.domain.Member; +import com.cruru.member.domain.repository.MemberRepository; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; import java.util.HashMap; import java.util.Map; import lombok.RequiredArgsConstructor; @@ -20,18 +30,82 @@ public class AuthService { private final TokenProvider tokenProvider; private final PasswordValidator passwordValidator; + private final TokenProperties tokenProperties; + private final RefreshTokenRepository refreshTokenRepository; + private final MemberRepository memberRepository; - public String createToken(Member member) { + public Token createAccessToken(Member member) { + Map claims = getClaims(member); + String token = tokenProvider.createToken(claims, tokenProperties.accessExpireLength()); + return new AccessToken(token); + } + + @Transactional + public Token createRefreshToken(Member member) { + if (refreshTokenRepository.existsByMember(member)) { + return rotateRefreshToken(member); + } + + Map claims = getClaims(member); + String token = tokenProvider.createToken(claims, tokenProperties.refreshExpireLength()); + + return refreshTokenRepository.save(new RefreshToken(token, member)); + } + + @Transactional + public TokenResponse refresh(String refreshToken) { + checkRefreshTokenExists(refreshToken); + + String email = extractEmail(refreshToken); + Member member = memberRepository.findByEmail(email) + .orElseThrow(); + validMemberRefreshToken(refreshToken, member); + return rotateTokens(member); + } + + private void checkRefreshTokenExists(String refreshToken) { + if (!isTokenSignatureValid(refreshToken)) { + throw new IllegalTokenException(); + } + + if (!refreshTokenRepository.existsByToken(refreshToken)) { + throw new IllegalTokenException(); + } + + if (isTokenExpired(refreshToken)) { + throw new LoginExpiredException(); + } + } + + private TokenResponse rotateTokens(Member member) { + Token accessToken = createAccessToken(member); + Token refreshToken = rotateRefreshToken(member); + return new TokenResponse(accessToken.getToken(), refreshToken.getToken()); + } + + private Token rotateRefreshToken(Member member) { + Map claims = getClaims(member); + String token = tokenProvider.createToken(claims, tokenProperties.refreshExpireLength()); + RefreshToken refreshToken = new RefreshToken(token, member); + + refreshTokenRepository.updateRefreshTokenByMember(refreshToken.getToken(), member); + return refreshToken; + } + + private Map getClaims(Member member) { Map claims = new HashMap<>(); claims.put(EMAIL_CLAIM, member.getEmail()); claims.put(ROLE_CLAIM, member.getRole().name()); + return claims; + } - return tokenProvider.createToken(claims); + public boolean isTokenExpired(String token) { + return tokenProvider.isTokenExpired(token); } - public boolean isTokenValid(String token) { + public boolean isTokenSignatureValid(String token) { try { - return tokenProvider.isAlive(token); + return tokenProvider.isSignatureValid(token); } catch (IllegalTokenException e) { return false; } @@ -41,19 +115,33 @@ public String extractEmail(String token) { return extractClaim(token, EMAIL_CLAIM); } + public String extractMemberRole(String token) { + return extractClaim(token, ROLE_CLAIM); + } + private String extractClaim(String token, String key) { - String claim = tokenProvider.extractClaim(token, key); + String claim; + try { + claim = tokenProvider.extractClaim(token, key); + } catch (ExpiredJwtException e) { + Claims claims = e.getClaims(); + return claims.get(key, String.class); + } if (claim == null) { throw new IllegalTokenException(); } return claim; } - public String extractMemberRole(String token) { - return extractClaim(token, ROLE_CLAIM); - } - public boolean isNotVerifiedPassword(String rawPassword, String encodedPassword) { return !passwordValidator.matches(rawPassword, encodedPassword); } + + private void validMemberRefreshToken(String refreshToken, Member member) { + RefreshToken foundToken = refreshTokenRepository.findByMember(member) + .orElseThrow(IllegalTokenException::new); + if (!foundToken.isSameToken(refreshToken)) { + throw new IllegalTokenException(); + } + } } diff --git a/backend/src/main/java/com/cruru/config/RedisConfig.java b/backend/src/main/java/com/cruru/config/RedisConfig.java new file mode 100644 index 000000000..88265f5ff --- /dev/null +++ b/backend/src/main/java/com/cruru/config/RedisConfig.java @@ -0,0 +1,43 @@ +package com.cruru.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisPassword; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Value("${spring.data.redis.host}") + private String host; + + @Value("${spring.data.redis.port}") + private int port; + + @Value("${spring.data.redis.password}") + private String password; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration(); + redisConfig.setHostName(host); + redisConfig.setPort(port); + redisConfig.setPassword(RedisPassword.of(password)); + return new LettuceConnectionFactory(redisConfig); + } + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(connectionFactory); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } +} diff --git a/backend/src/main/java/com/cruru/global/AuthenticationInterceptor.java b/backend/src/main/java/com/cruru/global/AuthenticationInterceptor.java index e12a389c5..68629cb9d 100644 --- a/backend/src/main/java/com/cruru/global/AuthenticationInterceptor.java +++ b/backend/src/main/java/com/cruru/global/AuthenticationInterceptor.java @@ -1,5 +1,6 @@ package com.cruru.global; +import com.cruru.auth.controller.response.TokenResponse; import com.cruru.auth.exception.IllegalCookieException; import com.cruru.auth.exception.LoginUnauthorizedException; import com.cruru.auth.service.AuthService; @@ -7,7 +8,9 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseCookie; import org.springframework.web.servlet.HandlerInterceptor; @RequiredArgsConstructor @@ -28,10 +31,26 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons return true; } + if (isValidTokenExpired(request)) { + refresh(request, response); + return true; + } + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; } + private void refresh(HttpServletRequest request, HttpServletResponse response) { + String token = cookieManager.extractRefreshToken(request); + TokenResponse tokenResponse = authService.refresh(token); + + ResponseCookie accessTokenCookie = cookieManager.createAccessTokenCookie(tokenResponse.accessToken()); + ResponseCookie refreshTokenCookie = cookieManager.createRefreshTokenCookie(tokenResponse.refreshToken()); + + response.addHeader(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()); + response.addHeader(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()); + } + private boolean isGetApplyformRequest(HttpServletRequest request) { return request.getRequestURI().matches(APPLYFORM_REQUEST_URI) && isGetRequest(request); } @@ -42,8 +61,17 @@ private boolean isOptionsRequest(HttpServletRequest request) { private boolean isAuthenticated(HttpServletRequest request) { try { - String token = cookieManager.extractToken(request); - return authService.isTokenValid(token); + String token = cookieManager.extractAccessToken(request); + return authService.isTokenSignatureValid(token) && !authService.isTokenExpired(token); + } catch (IllegalCookieException e) { + throw new LoginUnauthorizedException(); + } + } + + private boolean isValidTokenExpired(HttpServletRequest request) { + try { + String token = cookieManager.extractAccessToken(request); + return authService.isTokenSignatureValid(token) && authService.isTokenExpired(token); } catch (IllegalCookieException e) { throw new LoginUnauthorizedException(); } diff --git a/backend/src/main/java/com/cruru/global/LoginArgumentResolver.java b/backend/src/main/java/com/cruru/global/LoginArgumentResolver.java index ceca58810..d137b1b04 100644 --- a/backend/src/main/java/com/cruru/global/LoginArgumentResolver.java +++ b/backend/src/main/java/com/cruru/global/LoginArgumentResolver.java @@ -34,7 +34,7 @@ public LoginProfile resolveArgument( ) throws UnauthorizedException { HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); - String token = cookieManager.extractToken(request); + String token = cookieManager.extractAccessToken(request); String emailPayload = authService.extractEmail(token); MemberRole memberRolePayload = MemberRole.valueOf(authService.extractMemberRole(token)); diff --git a/backend/src/main/java/com/cruru/global/util/CookieManager.java b/backend/src/main/java/com/cruru/global/util/CookieManager.java index 92986a4bf..313114c67 100644 --- a/backend/src/main/java/com/cruru/global/util/CookieManager.java +++ b/backend/src/main/java/com/cruru/global/util/CookieManager.java @@ -14,7 +14,7 @@ public class CookieManager { private final CookieProperties cookieProperties; - public String extractToken(HttpServletRequest request) { + public String extractAccessToken(HttpServletRequest request) { Cookie[] cookies = extractCookie(request); return Arrays.stream(cookies) .filter(this::isAccessTokenCookie) @@ -36,7 +36,20 @@ private boolean isAccessTokenCookie(Cookie cookie) { return cookieProperties.accessTokenKey().equals(cookie.getName()); } - public ResponseCookie createTokenCookie(String token) { + public String extractRefreshToken(HttpServletRequest request) { + Cookie[] cookies = extractCookie(request); + return Arrays.stream(cookies) + .filter(this::isRefreshTokenCookie) + .findFirst() + .map(Cookie::getValue) + .orElseThrow(IllegalCookieException::new); + } + + private boolean isRefreshTokenCookie(Cookie cookie) { + return cookieProperties.refreshTokenKey().equals(cookie.getName()); + } + + public ResponseCookie createAccessTokenCookie(String token) { return ResponseCookie.from(cookieProperties.accessTokenKey(), token) .httpOnly(cookieProperties.httpOnly()) .secure(cookieProperties.secure()) @@ -47,7 +60,18 @@ public ResponseCookie createTokenCookie(String token) { .build(); } - public ResponseCookie clearTokenCookie() { + public ResponseCookie createRefreshTokenCookie(String refreshToken) { + return ResponseCookie.from(cookieProperties.refreshTokenKey(), refreshToken) + .httpOnly(cookieProperties.httpOnly()) + .secure(cookieProperties.secure()) + .domain(cookieProperties.domain()) + .path(cookieProperties.path()) + .sameSite(cookieProperties.sameSite()) + .maxAge(cookieProperties.maxAge()) + .build(); + } + + public ResponseCookie clearAccessTokenCookie() { return ResponseCookie.from(cookieProperties.accessTokenKey()) .httpOnly(cookieProperties.httpOnly()) .secure(cookieProperties.secure()) @@ -57,4 +81,15 @@ public ResponseCookie clearTokenCookie() { .maxAge(0) .build(); } + + public ResponseCookie clearRefreshTokenCookie() { + return ResponseCookie.from(cookieProperties.refreshTokenKey()) + .httpOnly(cookieProperties.httpOnly()) + .secure(cookieProperties.secure()) + .domain(cookieProperties.domain()) + .path(cookieProperties.path()) + .sameSite(cookieProperties.sameSite()) + .maxAge(0) + .build(); + } } diff --git a/backend/src/main/java/com/cruru/global/util/CookieProperties.java b/backend/src/main/java/com/cruru/global/util/CookieProperties.java index c3454930e..e89281fe1 100644 --- a/backend/src/main/java/com/cruru/global/util/CookieProperties.java +++ b/backend/src/main/java/com/cruru/global/util/CookieProperties.java @@ -5,6 +5,7 @@ @ConfigurationProperties("cookie") public record CookieProperties( String accessTokenKey, + String refreshTokenKey, boolean httpOnly, boolean secure, String domain, diff --git a/backend/src/main/java/com/cruru/process/controller/ProcessController.java b/backend/src/main/java/com/cruru/process/controller/ProcessController.java index 1d29db47c..3e7b17312 100644 --- a/backend/src/main/java/com/cruru/process/controller/ProcessController.java +++ b/backend/src/main/java/com/cruru/process/controller/ProcessController.java @@ -38,8 +38,8 @@ public ResponseEntity read( @RequestParam(name = "minScore", required = false, defaultValue = "0.00") Double minScore, @RequestParam(name = "maxScore", required = false, defaultValue = "5.00") Double maxScore, @RequestParam(name = "evaluationStatus", required = false, defaultValue = "ALL") String evaluationStatus, - @RequestParam(name = "sortByCreatedAt", required = false, defaultValue = "DESC") String sortByCreatedAt, - @RequestParam(name = "sortByScore", required = false, defaultValue = "DESC") String sortByScore, + @RequestParam(name = "sortByCreatedAt", required = false) String sortByCreatedAt, + @RequestParam(name = "sortByScore", required = false) String sortByScore, LoginProfile loginProfile ) { ProcessResponses processes = processFacade.readAllByDashboardId( diff --git a/backend/src/main/java/com/cruru/process/controller/response/ProcessResponses.java b/backend/src/main/java/com/cruru/process/controller/response/ProcessResponses.java index 15ac84f0e..5de82f4f0 100644 --- a/backend/src/main/java/com/cruru/process/controller/response/ProcessResponses.java +++ b/backend/src/main/java/com/cruru/process/controller/response/ProcessResponses.java @@ -1,6 +1,7 @@ package com.cruru.process.controller.response; import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.LocalDateTime; import java.util.List; public record ProcessResponses( @@ -10,7 +11,11 @@ public record ProcessResponses( @JsonProperty("processes") List processResponses, - String title + String title, + + LocalDateTime startDate, + + LocalDateTime endDate ) { } diff --git a/backend/src/main/java/com/cruru/process/facade/ProcessFacade.java b/backend/src/main/java/com/cruru/process/facade/ProcessFacade.java index 4bd4e071e..699f1a0ef 100644 --- a/backend/src/main/java/com/cruru/process/facade/ProcessFacade.java +++ b/backend/src/main/java/com/cruru/process/facade/ProcessFacade.java @@ -54,7 +54,13 @@ public ProcessResponses readAllByDashboardId( .map(process -> toProcessResponse(process, applicantCards)) .toList(); - return new ProcessResponses(applyForm.getId(), processResponses, applyForm.getTitle()); + return new ProcessResponses( + applyForm.getId(), + processResponses, + applyForm.getTitle(), + applyForm.getStartDate(), + applyForm.getEndDate() + ); } private ProcessResponse toProcessResponse(Process process, List applicantCards) { diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 6752c8733..20919a2dc 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -45,16 +45,23 @@ spring: file-size-threshold: 2KB max-file-size: 25MB max-request-size: 50MB + data: + redis: + port: ${REDIS_PORT} + host: ${REDIS_HOST} + password: ${REDIS_PASSWORD} security: jwt: token: secret-key: test - expire-length: 3600000 + access-expire-length: 3600000 + refresh-expire-length: 3600000 algorithm: HS256 cookie: - access-token-key: token + access-token-key: accessToken + refresh-token-key: refreshToken http-only: false secure: false domain: localhost @@ -100,8 +107,8 @@ spring: mail: host: smtp.gmail.com port: 587 - username: ${EMAIL_USERNAME} - password: ${EMAIL_PASSWORD} + username: ${EMAIL_USERNAME} + password: ${EMAIL_PASSWORD} properties: mail: smtp: @@ -114,16 +121,23 @@ spring: file-size-threshold: 2KB max-file-size: 25MB max-request-size: 50MB + data: + redis: + port: ${REDIS_PORT} + host: ${REDIS_HOST} + password: ${REDIS_PASSWORD} security: jwt: token: secret-key: ${JWT_TOKEN_SECRET_KEY} - expire-length: ${JWT_TOKEN_EXPIRE_CYCLE} + access-expire-length: ${ACCESS_TOKEN_EXPIRE_CYCLE} + refresh-expire-length: ${REFRESH_TOKEN_EXPIRE_CYCLE} algorithm: ${JWT_SIGN_ALGORITHM} cookie: access-token-key: ${COOKIE_ACCESS_TOKEN_KEY} + refresh-token-key: ${COOKIE_REFRESH_TOKEN_KEY} http-only: ${COOKIE_HTTP_ONLY} secure: ${COOKIE_SECURE} domain: ${COOKIE_DOMAIN} @@ -203,16 +217,22 @@ spring: file-size-threshold: 2KB max-file-size: 25MB max-request-size: 50MB - + data: + redis: + port: ${REDIS_PORT} + host: ${REDIS_HOST} + password: ${REDIS_PASSWORD} security: jwt: token: secret-key: ${JWT_TOKEN_SECRET_KEY} - expire-length: ${JWT_TOKEN_EXPIRE_CYCLE} + access-expire-length: ${ACCESS_TOKEN_EXPIRE_CYCLE} + refresh-expire-length: ${REFRESH_TOKEN_EXPIRE_CYCLE} algorithm: ${JWT_SIGN_ALGORITHM} cookie: access-token-key: ${COOKIE_ACCESS_TOKEN_KEY} + refresh-token-key: ${COOKIE_REFRESH_TOKEN_KEY} http-only: ${COOKIE_HTTP_ONLY} secure: ${COOKIE_SECURE} domain: ${COOKIE_DOMAIN} @@ -277,8 +297,8 @@ spring: mail: host: smtp.gmail.com port: 587 - username: ${EMAIL_USERNAME} - password: ${EMAIL_PASSWORD} + username: ${EMAIL_USERNAME} + password: ${EMAIL_PASSWORD} properties: mail: smtp: @@ -291,16 +311,22 @@ spring: file-size-threshold: 2KB max-file-size: 25MB max-request-size: 50MB - + data: + redis: + port: ${REDIS_PORT} + host: ${REDIS_HOST} + password: ${REDIS_PASSWORD} security: jwt: token: secret-key: ${JWT_TOKEN_SECRET_KEY} - expire-length: ${JWT_TOKEN_EXPIRE_CYCLE} + access-expire-length: ${ACCESS_TOKEN_EXPIRE_CYCLE} + refresh-expire-length: ${REFRESH_TOKEN_EXPIRE_CYCLE} algorithm: ${JWT_SIGN_ALGORITHM} cookie: access-token-key: ${COOKIE_ACCESS_TOKEN_KEY} + refresh-token-key: ${COOKIE_REFRESH_TOKEN_KEY} http-only: ${COOKIE_HTTP_ONLY} secure: ${COOKIE_SECURE} domain: ${COOKIE_DOMAIN} diff --git a/backend/src/main/resources/db/migration/V2_1_3__create_refresh_token_table.sql b/backend/src/main/resources/db/migration/V2_1_3__create_refresh_token_table.sql new file mode 100644 index 000000000..41a98406d --- /dev/null +++ b/backend/src/main/resources/db/migration/V2_1_3__create_refresh_token_table.sql @@ -0,0 +1,11 @@ +CREATE TABLE refresh_token +( + refresh_token_id BIGINT NOT NULL AUTO_INCREMENT, + created_date DATETIME(6), + member_id BIGINT NOT NULL, + updated_date DATETIME(6), + token VARCHAR(512) NOT NULL, + PRIMARY KEY (refresh_token_id) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; diff --git a/backend/src/main/resources/db/migration/V2_1_4__refresh_token_table_constraints.sql b/backend/src/main/resources/db/migration/V2_1_4__refresh_token_table_constraints.sql new file mode 100644 index 000000000..1186cce13 --- /dev/null +++ b/backend/src/main/resources/db/migration/V2_1_4__refresh_token_table_constraints.sql @@ -0,0 +1,10 @@ +ALTER TABLE refresh_token + ADD CONSTRAINT uk_refresh_token_member_id UNIQUE (member_id); + +ALTER TABLE refresh_token + ADD CONSTRAINT uk_refresh_token_token UNIQUE (token); + +ALTER TABLE refresh_token + ADD CONSTRAINT fk_refresh_token_to_member + FOREIGN KEY (member_id) + REFERENCES member (member_id); diff --git a/backend/src/test/java/com/cruru/applicant/controller/ApplicantControllerTest.java b/backend/src/test/java/com/cruru/applicant/controller/ApplicantControllerTest.java index 15bcb9a43..49c8796c5 100644 --- a/backend/src/test/java/com/cruru/applicant/controller/ApplicantControllerTest.java +++ b/backend/src/test/java/com/cruru/applicant/controller/ApplicantControllerTest.java @@ -77,12 +77,12 @@ void updateProcess() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(new ApplicantMoveRequest(List.of(applicant.getId()))) .filter(document( "applicant/move-process/", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("processId").description("지원자들이 옮겨질 프로세스의 id")), requestFields(fieldWithPath("applicantIds").description("프로세스를 옮길 지원자들의 id")) )) @@ -102,12 +102,12 @@ void updateApplicantProcess_processNotFound() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(new ApplicantMoveRequest(List.of(applicant.getId()))) .filter(document( "applicant/move-process-fail/process-not-found/", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("processId").description("존재하지 않는 프로세스의 id")), requestFields(fieldWithPath("applicantIds").description("프로세스를 옮길 지원자들의 id")) )) @@ -124,10 +124,10 @@ void read() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .filter(document( "applicant/read-profile", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("applicantId").description("지원자의 id")), responseFields( fieldWithPath("applicant.id").description("지원자의 id"), @@ -152,11 +152,11 @@ void read_applicantNotFound() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .filter(document( "applicant/read-profile-fail/applicant-not-found/", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("applicantId").description("존재하지 않는 지원자의 id")) )) .when().get("/v1/applicants/{applicantId}", invalidApplicantId) @@ -176,10 +176,10 @@ void readDetail() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .filter(document( "applicant/read-detail-profile", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("applicantId").description("지원자의 id")), responseFields( fieldWithPath("details").description("답변들") @@ -197,10 +197,10 @@ void readDetail_applicantNotFound() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .filter(document( "applicant/read-detail-profile-fail/applicant-not-found", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("applicantId").description("지원자의 id")) )) .when().get("/v1/applicants/{applicantId}/detail", invalidApplicantId) @@ -215,10 +215,10 @@ void reject() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .filter(document( "applicant/reject", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("applicantId").description("지원자의 id")) )) .when().patch("/v1/applicants/{applicantId}/reject", applicant.getId()) @@ -233,11 +233,11 @@ void reject_applicantNotFound() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .filter(document( "applicant/reject-fail/applicant-not-found/", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("applicantId").description("지원자의 id")) )) .when().patch("/v1/applicants/{applicantId}/reject", invalidApplicantId) @@ -254,11 +254,11 @@ void reject_alreadyReject() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .filter(document( "applicant/reject-fail/already-rejected/", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("applicantId").description("존재하지 않는 지원자의 id")) )) .when().patch("/v1/applicants/{applicantId}/reject", savedApplicant.getId()) @@ -273,10 +273,10 @@ void unreject() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .filter(document( "applicant/unreject", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("applicantId").description("지원자의 id")) )) .when().patch("/v1/applicants/{applicantId}/unreject", applicant.getId()) @@ -291,11 +291,11 @@ void unreject_applicantNotFound() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .filter(document( "applicant/unreject-fail/applicant-not-found/", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("applicantId").description("존재하지 않는 지원자의 id")) )) .when().patch("/v1/applicants/{applicantId}/unreject", invalidApplicantId) @@ -310,11 +310,11 @@ void unreject_notRejected() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .filter(document( "applicant/unreject-fail/applicant-not-rejected/", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("applicantId").description("불합격하지 않는 지원자의 id")) )) .when().patch("/v1/applicants/{applicantId}/unreject", applicant.getId()) @@ -333,12 +333,12 @@ void updateInformation() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(request) .filter(document( "applicant/change-info", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("applicantId").description("지원자의 id")) )) .when().patch("/v1/applicants/{applicantId}", applicant.getId()) @@ -357,12 +357,12 @@ void updateInformation_applicantNotFound() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(request) .filter(document( "applicant/change-info-fail/applicant-not-found/", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("applicantId").description("존재하지 않는 지원자의 id")) )) .when().patch("/v1/applicants/{applicantId}", invalidApplicantId) @@ -379,12 +379,12 @@ void rejectAll() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(request) .filter(document( "applicant/reject-all/", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), requestFields(fieldWithPath("applicantIds").description("불합격 시킬 지원자들의 id")) )) .when().patch("/v1/applicants/reject") @@ -399,12 +399,12 @@ void rejectAll_applicantNotFound() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(request) .filter(document( "applicant/reject-all-fail/applicant-not-found/", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), requestFields(fieldWithPath("applicantIds").description("존재하지 않는 지원자의 id")) )) .when().patch("/v1/applicants/reject") @@ -420,12 +420,12 @@ void rejectAll_alreadyReject() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(request) .filter(document( "applicant/reject-all-fail/already-rejected/", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), requestFields(fieldWithPath("applicantIds").description("이미 불합격한 지원자의 id")) )) .when().patch("/v1/applicants/reject") @@ -442,12 +442,12 @@ void unrejectAll() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(request) .filter(document( "applicant/unreject-all/", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), requestFields(fieldWithPath("applicantIds").description("불합격 해제시킬 지원자들의 id")) )) .when().patch("/v1/applicants/unreject") @@ -462,12 +462,12 @@ void unrejectAll_applicantNotFound() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(request) .filter(document( "applicant/unreject-all-fail/applicant-not-found/", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), requestFields(fieldWithPath("applicantIds").description("존재하지 않는 지원자의 id")) )) .when().patch("/v1/applicants/unreject") @@ -483,12 +483,12 @@ void unrejectAll_alreadyUnreject() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(request) .filter(document( "applicant/unreject-all-fail/already-unrejected/", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), requestFields(fieldWithPath("applicantIds").description("불합격하지 않은 지원자의 id")) )) .when().patch("/v1/applicants/unreject") diff --git a/backend/src/test/java/com/cruru/applicant/controller/EvaluationControllerTest.java b/backend/src/test/java/com/cruru/applicant/controller/EvaluationControllerTest.java index 70b3e95b5..18a824625 100644 --- a/backend/src/test/java/com/cruru/applicant/controller/EvaluationControllerTest.java +++ b/backend/src/test/java/com/cruru/applicant/controller/EvaluationControllerTest.java @@ -77,12 +77,12 @@ void create() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(request) .filter(document( "evaluation/create", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters( parameterWithName("processId").description("프로세스의 id"), parameterWithName("applicantId").description("지원자의 id") @@ -112,12 +112,12 @@ void create_applicantNotFound() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(request) .filter(document( "evaluation/create-fail/applicant-not-found", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters( parameterWithName("processId").description("프로세스의 id"), parameterWithName("applicantId").description("존재하지 않는 지원자의 id") @@ -147,12 +147,12 @@ void create_processNotFound() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(request) .filter(document( "evaluation/create-fail/process-not-found", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters( parameterWithName("processId").description("존재하지 않는 프로세스의 id"), parameterWithName("applicantId").description("지원자의 id") @@ -181,12 +181,12 @@ void create_invalidScore() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(request) .filter(document( "evaluation/create-fail/invalid-score", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters( parameterWithName("processId").description("프로세스의 id"), parameterWithName("applicantId").description("지원자의 id") @@ -209,11 +209,11 @@ void read() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .filter(document( "evaluation/read", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters( parameterWithName("processId").description("프로세스의 id"), parameterWithName("applicantId").description("지원자의 id") @@ -239,11 +239,11 @@ void read_applicantNotFound() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .filter(document( "evaluation/read-fail/applicant-not-found", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters( parameterWithName("processId").description("프로세스의 id"), parameterWithName("applicantId").description("지원자의 id") @@ -266,11 +266,11 @@ void read_processNotFound() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .filter(document( "evaluation/read-fail/process-not-found", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters( parameterWithName("processId").description("프로세스의 id"), parameterWithName("applicantId").description("지원자의 id") @@ -291,12 +291,12 @@ void update() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(request) .filter(document( "evaluation/update", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("evaluationId").description("평가의 id")), requestFields( fieldWithPath("score").description("평가 점수"), @@ -317,12 +317,12 @@ void update_evaluationNotFound() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(request) .filter(document( "evaluation/update-fail/evaluation-not-found", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("evaluationId").description("존재하지 않는 평가의 id")), requestFields( fieldWithPath("score").description("평가 점수"), @@ -344,12 +344,12 @@ void update_invalidScore() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(request) .filter(document( "evaluation/update-fail/invalid-score", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("evaluationId").description("평가의 id")), requestFields( fieldWithPath("score").description("적절하지 않은 평가 점수"), diff --git a/backend/src/test/java/com/cruru/applicant/domain/ApplicantSortOptionTest.java b/backend/src/test/java/com/cruru/applicant/domain/ApplicantSortOptionTest.java index 61cc2dd7a..81900d598 100644 --- a/backend/src/test/java/com/cruru/applicant/domain/ApplicantSortOptionTest.java +++ b/backend/src/test/java/com/cruru/applicant/domain/ApplicantSortOptionTest.java @@ -4,7 +4,6 @@ import com.cruru.applicant.domain.dto.ApplicantCard; import com.cruru.util.fixture.ApplicantCardFixture; -import com.cruru.util.fixture.DefaultFilterAndOrderFixture; import java.time.LocalDateTime; import java.util.Arrays; import java.util.List; @@ -33,9 +32,9 @@ void getCreatedAtComparator_asc(String sortByCreatedAt) { List applicantCards = Arrays.asList(applicantCard2, applicantCard1); // when - applicantCards.sort(ApplicantSortOption.getCombinedComparator( + applicantCards.sort(ApplicantSortOption.getComparator( sortByCreatedAt, - DefaultFilterAndOrderFixture.DEFAULT_SORT_BY_SCORE + null )); // then @@ -54,10 +53,9 @@ void getCreatedAtComparator_desc(String sortByCreatedAt) { List applicantCards = Arrays.asList(applicantCard1, applicantCard2); // when - applicantCards.sort(ApplicantSortOption.getCombinedComparator( + applicantCards.sort(ApplicantSortOption.getComparator( sortByCreatedAt, - DefaultFilterAndOrderFixture.DEFAULT_SORT_BY_SCORE - + null )); // then @@ -72,7 +70,7 @@ class getScoreComparatorTest { @DisplayName("지원자의 평균 점수 기준으로 오름차순으로 정렬한다.") @ParameterizedTest @ValueSource(strings = {"ASC", "asc"}) - void getScoreComparator_asc(String sortByCreatedAt) { + void getScoreComparator_asc(String sortByScore) { // given LocalDateTime createdAt = LocalDateTime.of(2024, 10, 2, 20, 0); ApplicantCard applicantCard1 = ApplicantCardFixture.evaluatedApplicantCard(createdAt); @@ -80,9 +78,9 @@ void getScoreComparator_asc(String sortByCreatedAt) { List applicantCards = Arrays.asList(applicantCard1, applicantCard2); // when - applicantCards.sort(ApplicantSortOption.getCombinedComparator( - DefaultFilterAndOrderFixture.DEFAULT_SORT_BY_CREATED_AT, - sortByCreatedAt + applicantCards.sort(ApplicantSortOption.getComparator( + null, + sortByScore )); // then @@ -92,7 +90,7 @@ void getScoreComparator_asc(String sortByCreatedAt) { @DisplayName("지원자의 평균 점수 기준으로 내림차순으로 정렬한다.") @ParameterizedTest @ValueSource(strings = {"DESC", "desc"}) - void getScoreComparator_desc(String sortByCreatedAt) { + void getScoreComparator_desc(String sortByScore) { // given LocalDateTime createdAt = LocalDateTime.of(2024, 10, 2, 20, 0); ApplicantCard applicantCard1 = ApplicantCardFixture.evaluatedApplicantCard(createdAt); @@ -100,9 +98,9 @@ void getScoreComparator_desc(String sortByCreatedAt) { List applicantCards = Arrays.asList(applicantCard2, applicantCard1); // when - applicantCards.sort(ApplicantSortOption.getCombinedComparator( - DefaultFilterAndOrderFixture.DEFAULT_SORT_BY_CREATED_AT, - sortByCreatedAt + applicantCards.sort(ApplicantSortOption.getComparator( + null, + sortByScore )); // then diff --git a/backend/src/test/java/com/cruru/applicant/service/ApplicantServiceTest.java b/backend/src/test/java/com/cruru/applicant/service/ApplicantServiceTest.java index f50bf80d2..8a40a553a 100644 --- a/backend/src/test/java/com/cruru/applicant/service/ApplicantServiceTest.java +++ b/backend/src/test/java/com/cruru/applicant/service/ApplicantServiceTest.java @@ -106,6 +106,8 @@ void findAllByProcess_filterAndOrder() { evaluationRepository.save(EvaluationFixture.fourPoints(process1, applicant2)); String evaluationStatus = "EVALUATED"; + String sortByCreatedAt = "DESC"; + String sortByScore = null; // when List applicantCards = applicantService.findApplicantCards( @@ -113,8 +115,8 @@ void findAllByProcess_filterAndOrder() { DefaultFilterAndOrderFixture.DEFAULT_MIN_SCORE, DefaultFilterAndOrderFixture.DEFAULT_MAX_SCORE, evaluationStatus, - DefaultFilterAndOrderFixture.DEFAULT_SORT_BY_CREATED_AT, - DefaultFilterAndOrderFixture.DEFAULT_SORT_BY_SCORE + sortByCreatedAt, + sortByScore ); // then diff --git a/backend/src/test/java/com/cruru/applyform/controller/ApplyFormControllerTest.java b/backend/src/test/java/com/cruru/applyform/controller/ApplyFormControllerTest.java index a5fe595b8..89790cb3c 100644 --- a/backend/src/test/java/com/cruru/applyform/controller/ApplyFormControllerTest.java +++ b/backend/src/test/java/com/cruru/applyform/controller/ApplyFormControllerTest.java @@ -391,7 +391,7 @@ void read() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .filter(document("applicant/read-applyform", pathParameters(parameterWithName("applyFormId").description("지원폼의 id")), @@ -417,7 +417,7 @@ void read_notFound() { // when&then RestAssured.given(spec).log().all() .contentType(ContentType.JSON) - .cookie("token", token) + .cookie("accessToken", token) .filter(document("applicant/read-applyform-fail/applyform-not-found", pathParameters(parameterWithName("applyFormId").description("존재하지 않는 지원폼의 id")) )) @@ -442,10 +442,10 @@ void update() { // when&then RestAssured.given(spec).log().all() .contentType(ContentType.JSON) - .cookie("token", token) + .cookie("accessToken", token) .body(request) .filter(document("applicant/update", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("applyFormId").description("지원폼의 id")), requestFields(APPLYFORM_WRITE_FIELD_DESCRIPTORS) )) @@ -469,7 +469,7 @@ void update_notFound() { // when&then RestAssured.given(spec).log().all() .contentType(ContentType.JSON) - .cookie("token", token) + .cookie("accessToken", token) .body(request) .filter(document("applicant/update-fail/applyform-not-found", pathParameters(parameterWithName("applyFormId").description("존재하지 않는 지원폼의 id")), diff --git a/backend/src/test/java/com/cruru/auth/aspect/AuthValidationAspectTest.java b/backend/src/test/java/com/cruru/auth/aspect/AuthValidationAspectTest.java index d0aab993c..ac17bea67 100644 --- a/backend/src/test/java/com/cruru/auth/aspect/AuthValidationAspectTest.java +++ b/backend/src/test/java/com/cruru/auth/aspect/AuthValidationAspectTest.java @@ -70,7 +70,7 @@ class AuthValidationAspectTest extends ControllerTest { @BeforeEach void setUp() { Member unauthorizedMember = memberRepository.save(MemberFixture.RUSH); - unauthorizedToken = authService.createToken(unauthorizedMember); + unauthorizedToken = authService.createAccessToken(unauthorizedMember).getToken(); clubRepository.save(ClubFixture.create(unauthorizedMember)); dashboard = dashboardRepository.save(DashboardFixture.backend(defaultClub)); applyForm = applyFormRepository.save(ApplyFormFixture.backend(dashboard)); @@ -94,7 +94,7 @@ void setUp() { @Test void testReadByRequestParam_Success() { RestAssured.given().log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .when().get("/auth-test/test1?applyformId=" + applyForm.getId()) .then().log().all().statusCode(200); @@ -105,7 +105,7 @@ void testReadByRequestParam_Success() { void testReadByRequestParam_Forbidden() { RestAssured.given().log().all() - .cookie("token", unauthorizedToken) + .cookie("accessToken", unauthorizedToken) .contentType(ContentType.JSON) .when().get("/auth-test/test1?applyformId=" + applyForm.getId()) .then().log().all().statusCode(403); @@ -115,7 +115,7 @@ void testReadByRequestParam_Forbidden() { @Test void testReadByPathVariable_Success() { RestAssured.given().log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .when().get("/auth-test/test2/" + applyForm.getId()) .then().log().all().statusCode(200); @@ -125,7 +125,7 @@ void testReadByPathVariable_Success() { @Test void testReadByPathVariable_Forbidden() { RestAssured.given().log().all() - .cookie("token", unauthorizedToken) + .cookie("accessToken", unauthorizedToken) .contentType(ContentType.JSON) .when().get("/auth-test/test2/" + applyForm.getId()) .then().log().all().statusCode(403); @@ -137,7 +137,7 @@ void testReadByRequestBody_Success() { RestAssured.given().log().all() .contentType(ContentType.JSON) - .cookie("token", token) + .cookie("accessToken", token) .body(authTestDto) .when().get("/auth-test/test3") .then().log().all().statusCode(200); @@ -148,7 +148,7 @@ void testReadByRequestBody_Success() { void testReadByRequestBody_Forbidden() { RestAssured.given().log().all() .contentType(ContentType.JSON) - .cookie("token", unauthorizedToken) + .cookie("accessToken", unauthorizedToken) .body(authTestDto) .when().get("/auth-test/test3") .then().log().all().statusCode(403); @@ -159,7 +159,7 @@ void testReadByRequestBody_Forbidden() { void testReadByAllRequestType_Success() { RestAssured.given().log().all() .contentType(ContentType.JSON) - .cookie("token", token) + .cookie("accessToken", token) .body(authTestDto) .when().get("/auth-test/test4/" + dashboard.getId() + "?applyformId=" + applyForm.getId()) .then().log().all().statusCode(200); @@ -170,7 +170,7 @@ void testReadByAllRequestType_Success() { void testReadByAllRequestType_Forbidden() { RestAssured.given().log().all() .contentType(ContentType.JSON) - .cookie("token", unauthorizedToken) + .cookie("accessToken", unauthorizedToken) .body(authTestDto) .when().get("/auth-test/test4/" + dashboard.getId() + "?applyformId=" + applyForm.getId()) .then().log().all().statusCode(403); @@ -201,7 +201,7 @@ void readByAllRequestType_SomeOfForbidden() { RestAssured.given().log().all() .contentType(ContentType.JSON) - .cookie("token", unauthorizedToken) + .cookie("accessToken", unauthorizedToken) .param("applyformId", applyForm.getId()) .body(unauthorizedAuthTestDto) .when().get("/auth-test/test4/" + dashboard.getId() + "?applyformId=" + applyForm.getId()) diff --git a/backend/src/test/java/com/cruru/auth/controller/AuthControllerTest.java b/backend/src/test/java/com/cruru/auth/controller/AuthControllerTest.java index 2466413fd..fddc5e998 100644 --- a/backend/src/test/java/com/cruru/auth/controller/AuthControllerTest.java +++ b/backend/src/test/java/com/cruru/auth/controller/AuthControllerTest.java @@ -11,6 +11,8 @@ import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import com.cruru.auth.controller.request.LoginRequest; +import com.cruru.auth.domain.Token; +import com.cruru.auth.service.AuthService; import com.cruru.club.domain.repository.ClubRepository; import com.cruru.member.domain.Member; import com.cruru.member.domain.repository.MemberRepository; @@ -60,7 +62,10 @@ void login() { ), responseFields(fieldWithPath("clubId").description("동아리의 id")), responseHeaders(headerWithName("Set-Cookie").description("인증 쿠키 설정")), - responseCookies(cookieWithName("token").description("사용자 토큰")) + responseCookies( + cookieWithName("accessToken").description("Access Token"), + cookieWithName("refreshToken").description("Refresh Token") + ) )) .when().post("/v1/auth/login") .then().log().all().statusCode(200); @@ -111,10 +116,10 @@ void login_emailNotFound() { void logout() { // given&when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .filter(document("auth/logout", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), responseHeaders(headerWithName("Set-Cookie").description("인증 해제 쿠키 설정")) )) .when().post("/v1/auth/logout") diff --git a/backend/src/test/java/com/cruru/auth/domain/repository/RefreshTokenRepositoryTest.java b/backend/src/test/java/com/cruru/auth/domain/repository/RefreshTokenRepositoryTest.java new file mode 100644 index 000000000..6ced43cb8 --- /dev/null +++ b/backend/src/test/java/com/cruru/auth/domain/repository/RefreshTokenRepositoryTest.java @@ -0,0 +1,66 @@ +package com.cruru.auth.domain.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.cruru.auth.domain.RefreshToken; +import com.cruru.member.domain.Member; +import com.cruru.member.domain.repository.MemberRepository; +import com.cruru.util.RepositoryTest; +import com.cruru.util.fixture.MemberFixture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@DisplayName("리프레쉬 토큰 레포지터리 테스트") +class RefreshTokenRepositoryTest extends RepositoryTest { + + @Autowired + private RefreshTokenRepository refreshTokenRepository; + + @Autowired + private MemberRepository memberRepository; + + private Member member; + + @BeforeEach + void setUp() { + refreshTokenRepository.deleteAllInBatch(); + memberRepository.deleteAllInBatch(); + + member = memberRepository.save(MemberFixture.DOBBY); + } + + @DisplayName("이미 DB에 저장되어 있는 ID를 가진 토큰을 저장하면, 해당 ID의 토큰은 후에 작성된 정보로 업데이트한다.") + @Test + void sameIdUpdate() { + //given + String token = "token"; + RefreshToken saved = refreshTokenRepository.save(new RefreshToken(token, member)); + + //when + String changedToken = "changedToken"; + RefreshToken refreshToken = new RefreshToken(saved.getId(), changedToken, null); + refreshTokenRepository.save(refreshToken); + + //then + RefreshToken foundToken = refreshTokenRepository.findById(saved.getId()).get(); + assertThat(foundToken.getToken()).isEqualTo(changedToken); + } + + @DisplayName("ID가 없는 사용자를 저장하면, ID를 순차적으로 부여하여 저장한다.") + @Test + void saveNoId() { + //given + Member member1 = memberRepository.save(MemberFixture.RUSH); + RefreshToken refreshToken1 = new RefreshToken("token", member); + RefreshToken refreshToken2 = new RefreshToken("token2", member1); + + //when + RefreshToken savedrefreshToken1 = refreshTokenRepository.save(refreshToken1); + RefreshToken savedrefreshToken2 = refreshTokenRepository.save(refreshToken2); + + //then + assertThat(savedrefreshToken1.getId() + 1).isEqualTo(savedrefreshToken2.getId()); + } +} diff --git a/backend/src/test/java/com/cruru/auth/security/JwtTokenProviderTest.java b/backend/src/test/java/com/cruru/auth/security/JwtTokenProviderTest.java index 773965b17..272c4bf20 100644 --- a/backend/src/test/java/com/cruru/auth/security/JwtTokenProviderTest.java +++ b/backend/src/test/java/com/cruru/auth/security/JwtTokenProviderTest.java @@ -42,10 +42,13 @@ void setUp() { @DisplayName("토큰이 정상적으로 생성되는지 확인한다") @Test void create() { - // given&when - String token = jwtTokenProvider.createToken(claims); + // given + long expireLength = 1209600000; + + // when + String token = jwtTokenProvider.createToken(claims, expireLength); Claims extractedClaims = Jwts.parser() - .setSigningKey(TEST_SECRET_KEY.getBytes()) + .setSigningKey(TEST_SECRET_KEY) .parseClaimsJws(token) .getBody(); @@ -64,7 +67,8 @@ void create() { @Test void extractEmailAndRole() { // given - String token = jwtTokenProvider.createToken(claims); + long expireLength = 1209600000; + String token = jwtTokenProvider.createToken(claims, expireLength); // when String email = jwtTokenProvider.extractClaim(token, EMAIL_CLAIM); @@ -82,12 +86,12 @@ void extractEmailAndRole() { @DisplayName("만료된 토큰을 검증한다.") @Test - void isAlive() { + void isTokenExpired() { // given String expiredToken = generateExpiredToken(); // when&then - assertThat(jwtTokenProvider.isAlive(expiredToken)).isFalse(); + assertThat(jwtTokenProvider.isTokenExpired(expiredToken)).isTrue(); } private String generateExpiredToken() { @@ -98,7 +102,7 @@ private String generateExpiredToken() { .addClaims(claims) .setIssuedAt(now) .setExpiration(validity) - .signWith(SignatureAlgorithm.HS256, TEST_SECRET_KEY.getBytes()) + .signWith(SignatureAlgorithm.HS256, TEST_SECRET_KEY) .compact(); } @@ -106,9 +110,10 @@ private String generateExpiredToken() { @Test void isAlive_notValid() { // given - String notExpiredToken = jwtTokenProvider.createToken(claims); + long expireLength = 1209600000; + String notExpiredToken = jwtTokenProvider.createToken(claims, expireLength); // when&then - assertThat(jwtTokenProvider.isAlive(notExpiredToken)).isTrue(); + assertThat(jwtTokenProvider.isTokenExpired(notExpiredToken)).isFalse(); } } diff --git a/backend/src/test/java/com/cruru/auth/service/AuthServiceTest.java b/backend/src/test/java/com/cruru/auth/service/AuthServiceTest.java new file mode 100644 index 000000000..50bd48549 --- /dev/null +++ b/backend/src/test/java/com/cruru/auth/service/AuthServiceTest.java @@ -0,0 +1,203 @@ +package com.cruru.auth.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.cruru.auth.controller.response.TokenResponse; +import com.cruru.auth.domain.AccessToken; +import com.cruru.auth.domain.RefreshToken; +import com.cruru.auth.domain.Token; +import com.cruru.auth.domain.repository.RefreshTokenRepository; +import com.cruru.auth.exception.IllegalTokenException; +import com.cruru.member.domain.Member; +import com.cruru.member.domain.repository.MemberRepository; +import com.cruru.util.ServiceTest; +import com.cruru.util.fixture.MemberFixture; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@DisplayName("AuthService 테스트") +class AuthServiceTest extends ServiceTest { + + private static final String TEST_SECRET_KEY = "test"; + private static final String EMAIL_CLAIM = "email"; + private static final String ROLE_CLAIM = "role"; + + @Autowired + private AuthService authService; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private RefreshTokenRepository refreshTokenRepository; + + private Member member; + + private Map claims; + + @BeforeEach + void setUp() { + member = memberRepository.save(MemberFixture.DOBBY); + claims = new HashMap<>(); + claims.put(EMAIL_CLAIM, "email@example.com"); + claims.put(ROLE_CLAIM, "ADMIN"); + } + + @DisplayName("AccessToken을 생성한다.") + @Test + void createAccessToken() { + // given&when + Token accessToken = authService.createAccessToken(member); + + // then + assertAll( + () -> assertThat(accessToken).isInstanceOf(AccessToken.class), + () -> assertThat(accessToken.getToken()).isNotEmpty() + ); + } + + @DisplayName("토큰이 유효한지 확인한다.") + @Test + void isTokenSignatureValid() { + // given + Token accessToken = authService.createAccessToken(member); + + // when + boolean isValid = authService.isTokenSignatureValid(accessToken.getToken()); + + // then + assertThat(isValid).isTrue(); + } + + @DisplayName("토큰이 유효하지 않으면 false를 반환한다.") + @Test + void isTokenInvalid() { + // given + String invalidToken = "invalidToken"; + + // when + boolean isValid = authService.isTokenSignatureValid(invalidToken); + + // then + assertThat(isValid).isFalse(); + } + + @DisplayName("토큰에서 이메일을 추출한다.") + @Test + void extractEmail() { + // given + Token accessToken = authService.createAccessToken(member); + + // when + String email = authService.extractEmail(accessToken.getToken()); + + // then + assertThat(email).isEqualTo(member.getEmail()); + } + + @DisplayName("토큰에서 역할을 추출한다.") + @Test + void extractRole() { + // given + Token accessToken = authService.createAccessToken(member); + + // when + String role = authService.extractMemberRole(accessToken.getToken()); + + // then + assertThat(role).isEqualTo(member.getRole().name()); + } + + @DisplayName("잘못된 토큰에서 이메일 추출 시 예외를 발생시킨다.") + @Test + void extractClaimThrowsIllegalTokenException() { + // given + String invalidToken = "invalidToken"; + + // when & then + assertThatThrownBy(() -> authService.extractEmail(invalidToken)) + .isInstanceOf(IllegalTokenException.class); + } + + @DisplayName("비밀번호가 일치하지 않으면 true를 반환한다.") + @Test + void isNotVerifiedPassword() { + // given + String rawPassword = "plainPassword"; + String encodedPassword = "encodedPassword123"; + + // when + boolean result = authService.isNotVerifiedPassword(rawPassword, encodedPassword); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("Refresh Token 갱신에 성공한다.") + @Test + void refresh() throws InterruptedException { + // given + Token refreshToken = refreshTokenRepository.save((RefreshToken) authService.createRefreshToken(member)); + + // when + Thread.sleep(1000); + TokenResponse tokenResponse = authService.refresh(refreshToken.getToken()); + + // then + assertThat(refreshToken.getToken()).isNotEqualTo(tokenResponse.refreshToken()); + } + + @DisplayName("토큰의 만료 여부를 검증한다.") + @Test + void isTokenExpired() { + // given + String expiredToken = generateExpiredToken(); + + // when + boolean expired = authService.isTokenExpired(expiredToken); + + // then + assertThat(expired).isTrue(); + } + + @DisplayName("만료된 토큰의 클레임을 추출한다.") + @Test + void getEmail_TokenExpired() { + // given + String token = generateExpiredToken(); + + // when + String email = authService.extractEmail(token); + String role = authService.extractMemberRole(token); + + // then + String expectedRole = (String) claims.get(ROLE_CLAIM); + String expectedEmail = (String) claims.get(EMAIL_CLAIM); + + assertAll( + () -> assertThat(email).isEqualTo(expectedEmail), + () -> assertThat(role).isEqualTo(expectedRole) + ); + } + + private String generateExpiredToken() { + Date now = new Date(); + Date validity = new Date(now.getTime() - 3600000); // 1시간 전 만료 + + return Jwts.builder() + .addClaims(claims) + .setIssuedAt(now) + .setExpiration(validity) + .signWith(SignatureAlgorithm.HS256, TEST_SECRET_KEY) + .compact(); + } +} diff --git a/backend/src/test/java/com/cruru/club/controller/ClubControllerTest.java b/backend/src/test/java/com/cruru/club/controller/ClubControllerTest.java index 036e5b614..4d3435748 100644 --- a/backend/src/test/java/com/cruru/club/controller/ClubControllerTest.java +++ b/backend/src/test/java/com/cruru/club/controller/ClubControllerTest.java @@ -36,11 +36,11 @@ void create() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(request) .filter(document("club/create/", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters(parameterWithName("memberId").description("동아리를 생성할 사용자의 id")), requestFields(fieldWithPath("name").description("생성할 동아리의 이름")) )) @@ -59,11 +59,11 @@ void create_memberNotFound() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(request) .filter(document("club/create-fail/member-not-found/", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters(parameterWithName("memberId").description("존재하지 않는 사용자의 id")), requestFields(fieldWithPath("name").description("생성할 동아리의 이름")) )) @@ -82,11 +82,11 @@ void create_invalidName() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(request) .filter(document("club/create-fail/invalid-name/", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters(parameterWithName("memberId").description("동아리를 생성할 사용자의 id")), requestFields(fieldWithPath("name").description("조건에 맞지 않는 동아리의 이름")) )) diff --git a/backend/src/test/java/com/cruru/dashboard/controller/DashboardControllerTest.java b/backend/src/test/java/com/cruru/dashboard/controller/DashboardControllerTest.java index d56a5fd9e..48b86540f 100644 --- a/backend/src/test/java/com/cruru/dashboard/controller/DashboardControllerTest.java +++ b/backend/src/test/java/com/cruru/dashboard/controller/DashboardControllerTest.java @@ -172,11 +172,11 @@ void create() { // when&then RestAssured.given(spec).log().all() .contentType(ContentType.JSON) - .cookie("token", token) + .cookie("accessToken", token) .body(request) .filter(document( "dashboard/create", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters(parameterWithName("clubId").description("동아리의 id")), requestFields( fieldWithPath("title").description("공고 제목"), @@ -213,11 +213,11 @@ void create_invalidQuestionCreateRequest(QuestionCreateRequest invalidQuestionCr // when&then RestAssured.given(spec).log().all() .contentType(ContentType.JSON) - .cookie("token", token) + .cookie("accessToken", token) .body(request) .filter(document( "dashboard/create-fail/invalid-question", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters(parameterWithName("clubId").description("동아리의 id")), requestFields( fieldWithPath("title").description("공고 제목"), @@ -252,11 +252,11 @@ void create_invalidClub() { // when&then RestAssured.given(spec).log().all() .contentType(ContentType.JSON) - .cookie("token", token) + .cookie("accessToken", token) .body(request) .filter(document( "dashboard/create-fail/club-not-found", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters(parameterWithName("clubId").description("존재하지 않는 동아리 id")), requestFields( fieldWithPath("title").description("공고 제목"), @@ -281,10 +281,10 @@ void readDashboards_success() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .filter(document( "dashboard/read", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters(parameterWithName("clubId").description("동아리의 id")), responseFields( fieldWithPath("clubName").description("동아리명"), @@ -311,10 +311,10 @@ void delete() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .filter(document( "dashboard/delete", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("dashboardId").description("삭제할 대시보드의 id")) )) .when().delete("/v1/dashboards/{dashboardId}", dashboard.getId()) @@ -329,10 +329,10 @@ void delete_notFound() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .filter(document( "dashboard/delete/not-found", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("dashboardId").description("존재하지 않는 대시보드의 id")) )) .when().delete("/v1/dashboards/{dashboardId}", invalidId) diff --git a/backend/src/test/java/com/cruru/email/controller/EmailControllerTest.java b/backend/src/test/java/com/cruru/email/controller/EmailControllerTest.java index 1ce44975a..aaa4026a0 100644 --- a/backend/src/test/java/com/cruru/email/controller/EmailControllerTest.java +++ b/backend/src/test/java/com/cruru/email/controller/EmailControllerTest.java @@ -35,7 +35,7 @@ void send() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .multiPart("clubId", defaultClub.getId()) .multiPart("applicantIds", applicant.getId()) .multiPart("subject", subject) @@ -43,7 +43,7 @@ void send() { .multiPart("files", file) .contentType(ContentType.MULTIPART) .filter(document("email/send", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), requestParts( partWithName("clubId").description("발송 동아리 id"), partWithName("applicantIds").description("수신자 id 목록"), @@ -67,7 +67,7 @@ void send_invalidRequest() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .multiPart("clubId", defaultClub.getId()) .multiPart("applicantIds", invalidId) .multiPart("subject", subject) @@ -75,7 +75,7 @@ void send_invalidRequest() { .multiPart("files", file) .contentType(ContentType.MULTIPART) .filter(document("email/send-fail/invalid-email", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), requestParts( partWithName("clubId").description("발송 동아리 id"), partWithName("applicantIds").description("적절하지 않은 수신자 id가 포함된 목록"), @@ -101,7 +101,7 @@ void send_clubNotExist() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .multiPart("clubId", invalidId) .multiPart("applicantIds", email) .multiPart("subject", subject) @@ -109,7 +109,7 @@ void send_clubNotExist() { .multiPart("files", file) .contentType(ContentType.MULTIPART) .filter(document("email/send-fail/club-not-found", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), requestParts( partWithName("clubId").description("존재하지 않는 발송 동아리 id"), partWithName("applicantIds").description("수신자 id 목록"), diff --git a/backend/src/test/java/com/cruru/process/controller/ProcessControllerTest.java b/backend/src/test/java/com/cruru/process/controller/ProcessControllerTest.java index c6709c1f9..2777339a1 100644 --- a/backend/src/test/java/com/cruru/process/controller/ProcessControllerTest.java +++ b/backend/src/test/java/com/cruru/process/controller/ProcessControllerTest.java @@ -93,15 +93,17 @@ void read() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .filter(document( "process/read", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters(parameterWithName("dashboardId").description("대시보드의 id")), responseFields( fieldWithPath("applyFormId").description("지원폼의 id"), fieldWithPath("processes").description("프로세스 목록"), - fieldWithPath("title").description("지원폼의 제목") + fieldWithPath("title").description("지원폼의 제목"), + fieldWithPath("startDate").description("지원폼 모집 시작일"), + fieldWithPath("endDate").description("지원폼 모집 종료일") ).andWithPrefix("processes[].", PROCESS_RESPONSE_FIELD_DESCRIPTORS) .andWithPrefix("processes[].applicants[]", APPLICANT_RESPONSE_FIELD_DESCRIPTORS) )) @@ -109,7 +111,7 @@ void read() { .then().log().all().statusCode(200); } - @DisplayName("프로세스 필터링 및 정렬 조회 성공 시, 200을 응답한다.") + @DisplayName("프로세스 목록 조회 성공 시, 200을 응답한다.") @Test void read_filterAndOrder() { // given @@ -119,35 +121,39 @@ void read_filterAndOrder() { ProcessFixture.applyType(dashboard) )); applicantRepository.save(ApplicantFixture.pendingDobby(processes.get(0))); + String sortByCreatedAt = "DESC"; + String sortByScore = null; String url = String.format("/v1/processes?dashboardId=%d&minScore=%.2f&maxScore=%.2f" + "&evaluationStatus=%s&sortByCreatedAt=%s&sortByScore=%s", dashboard.getId(), DefaultFilterAndOrderFixture.DEFAULT_MIN_SCORE, DefaultFilterAndOrderFixture.DEFAULT_MAX_SCORE, DefaultFilterAndOrderFixture.DEFAULT_EVALUATION_STATUS, - DefaultFilterAndOrderFixture.DEFAULT_SORT_BY_CREATED_AT, - DefaultFilterAndOrderFixture.DEFAULT_SORT_BY_SCORE + sortByCreatedAt, + sortByScore ); // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .filter(document( "process/read-filter-and-order", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters( parameterWithName("dashboardId").description("대시보드의 id, required=true"), parameterWithName("minScore").description("지원자 최소 평균 점수: 0.00(default) ~ 5.00, required=false").optional(), parameterWithName("maxScore").description("지원자 최대 평균 점수: 0.00 ~ 5.00(default), required=false").optional(), parameterWithName("evaluationStatus").description( - "지원자 평가 유무: ALL(default), NOT_EVALUATION, EVALUATED, required=false").optional(), - parameterWithName("sortByCreatedAt").description("지원자 지원 날짜 정렬 조건: DESC(default), ASC, required=false").optional(), - parameterWithName("sortByScore").description("지원자 평균 점수 정렬 조건: DESC(default), ASC, required=false").optional() + "지원자 평가 유무: ALL(default), NOT_EVALUATED, EVALUATED, required=false").optional(), + parameterWithName("sortByCreatedAt").description("지원자 지원 날짜 정렬 조건: DESC, ASC, required=false").optional(), + parameterWithName("sortByScore").description("지원자 평균 점수 정렬 조건: DESC, ASC, required=false").optional() ), responseFields( fieldWithPath("applyFormId").description("지원폼의 id"), fieldWithPath("processes").description("프로세스 목록"), - fieldWithPath("title").description("지원폼의 제목") + fieldWithPath("title").description("지원폼의 제목"), + fieldWithPath("startDate").description("지원폼 모집 시작일"), + fieldWithPath("endDate").description("지원폼 모집 종료일") ).andWithPrefix("processes[].", PROCESS_RESPONSE_FIELD_DESCRIPTORS) .andWithPrefix("processes[].applicants[]", APPLICANT_RESPONSE_FIELD_DESCRIPTORS) )) @@ -164,10 +170,10 @@ void read_dashboardNotFound() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .filter(document( "process/read-fail/dashboard-not-found", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters(parameterWithName("dashboardId").description("존재하지 않는 대시보드의 id")) )) .when().get(url) @@ -183,12 +189,12 @@ void create() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(processCreateRequest) .filter(document( "process/create", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters(parameterWithName("dashboardId").description("대시보드의 id")), requestFields(PROCESS_CREATE_FIELD_DESCRIPTORS) )) @@ -205,12 +211,12 @@ void create_dashboardNotFound() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(processCreateRequest) .filter(document( "process/create-fail/dashboard-not-found", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters(parameterWithName("dashboardId").description("대시보드의 id")), requestFields(PROCESS_CREATE_FIELD_DESCRIPTORS) )) @@ -227,12 +233,12 @@ void create_invalidName() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(processCreateRequest) .filter(document( "process/create-fail/invalid-name", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters(parameterWithName("dashboardId").description("대시보드의 id")), requestFields( fieldWithPath("processName").description("부적절한 프로세스명"), @@ -261,12 +267,12 @@ void create_processCountOvered() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(processCreateRequest) .filter(document( "process/create-fail/process-count-overed/", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters(parameterWithName("dashboardId").description("생성할 프로세스의 대시보드 id")), requestFields(PROCESS_CREATE_FIELD_DESCRIPTORS) )) @@ -284,12 +290,12 @@ void update() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(processUpdateRequest) .filter(document( "process/update", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("processId").description("수정될 프로세스의 id")), requestFields( fieldWithPath("processName").description("프로세스명"), @@ -311,12 +317,12 @@ void update_invalidName() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(processUpdateRequest) .filter(document( "process/update-fail/invalid-name", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("processId").description("수정될 프로세스의 id")), requestFields( fieldWithPath("processName").description("조건에 맞지 않는 프로세스 이름"), @@ -337,12 +343,12 @@ void update_processNotFound() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(processUpdateRequest) .filter(document( "process/update-fail/process-not-found", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("processId").description("수정될 프로세스의 id")), requestFields( fieldWithPath("processName").description("조건에 맞지 않는 프로세스 이름"), @@ -361,10 +367,10 @@ void delete() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .filter(document( "process/delete", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("processId").description("생성할 프로세스의 대시보드 id")) )) .when().delete("/v1/processes/{processId}", process.getId()) @@ -379,10 +385,10 @@ void delete_processNotFound() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .filter(document( "process/delete-fail/process-not-found", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("processId").description("삭제할 프로세스의 id")) )) .when().delete("/v1/processes/{processId}", invalidId) @@ -397,10 +403,10 @@ void delete_endOrder() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .filter(document( "process/delete-fail/process-order-first-or-last", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("processId").description("삭제할 프로세스의 id")) )) .when().delete("/v1/processes/{processId}", process.getId()) @@ -416,10 +422,10 @@ void delete_applicantExist() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .filter(document( "process/delete-fail/process-applicant-exist", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("processId").description("삭제할 프로세스의 id")) )) .when().delete("/v1/processes/{processId}", process.getId()) diff --git a/backend/src/test/java/com/cruru/process/facade/ProcessFacadeTest.java b/backend/src/test/java/com/cruru/process/facade/ProcessFacadeTest.java index d7e928ec1..18320d432 100644 --- a/backend/src/test/java/com/cruru/process/facade/ProcessFacadeTest.java +++ b/backend/src/test/java/com/cruru/process/facade/ProcessFacadeTest.java @@ -93,6 +93,8 @@ void readAllByDashboardId() { EvaluationFixture.fourPoints(process2, applicant2) ); evaluationRepository.saveAll(evaluations2); + String sortByCreatedAt = "DESC"; + String sortByScore = null; // when ProcessResponses processResponses = processFacade.readAllByDashboardId( @@ -100,8 +102,8 @@ void readAllByDashboardId() { DefaultFilterAndOrderFixture.DEFAULT_MIN_SCORE, DefaultFilterAndOrderFixture.DEFAULT_MAX_SCORE, DefaultFilterAndOrderFixture.DEFAULT_EVALUATION_STATUS, - DefaultFilterAndOrderFixture.DEFAULT_SORT_BY_CREATED_AT, - DefaultFilterAndOrderFixture.DEFAULT_SORT_BY_SCORE + sortByCreatedAt, + sortByScore ); // then diff --git a/backend/src/test/java/com/cruru/question/controller/QuestionControllerTest.java b/backend/src/test/java/com/cruru/question/controller/QuestionControllerTest.java index 12ba3980c..0a12abcea 100644 --- a/backend/src/test/java/com/cruru/question/controller/QuestionControllerTest.java +++ b/backend/src/test/java/com/cruru/question/controller/QuestionControllerTest.java @@ -68,11 +68,11 @@ void update() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(questionUpdateRequests) .filter(document("question/update", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters(parameterWithName("applyformId").description("질문을 변경할 지원폼의 id")), requestFields(fieldWithPath("questions").description("변경할 질문들")) .andWithPrefix("questions[].", QUESTION_FIELD_DESCRIPTORS) @@ -103,11 +103,11 @@ void update_applyFormNotFound() { // when&then RestAssured.given(spec).log().all() - .cookie("token", token) + .cookie("accessToken", token) .contentType(ContentType.JSON) .body(questionUpdateRequests) .filter(document("question/update", - requestCookies(cookieWithName("token").description("사용자 토큰")), + requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters(parameterWithName("applyformId").description("존재하지 않는 지원폼의 id")), requestFields(fieldWithPath("questions").description("변경할 질문들")) .andWithPrefix("questions[].", QUESTION_FIELD_DESCRIPTORS) diff --git a/backend/src/test/java/com/cruru/util/ControllerTest.java b/backend/src/test/java/com/cruru/util/ControllerTest.java index 66659a2f6..7eaa2a273 100644 --- a/backend/src/test/java/com/cruru/util/ControllerTest.java +++ b/backend/src/test/java/com/cruru/util/ControllerTest.java @@ -66,7 +66,7 @@ void createDefaultLoginMember() { dbCleaner.truncateEveryTable(); defaultMember = memberRepository.save(MemberFixture.ADMIN); defaultClub = clubRepository.save(ClubFixture.create(defaultMember)); - token = authService.createToken(defaultMember); + token = authService.createAccessToken(defaultMember).getToken(); } @BeforeEach diff --git a/backend/src/test/java/com/cruru/util/fixture/DefaultFilterAndOrderFixture.java b/backend/src/test/java/com/cruru/util/fixture/DefaultFilterAndOrderFixture.java index 7734c1e85..ab13c8ac3 100644 --- a/backend/src/test/java/com/cruru/util/fixture/DefaultFilterAndOrderFixture.java +++ b/backend/src/test/java/com/cruru/util/fixture/DefaultFilterAndOrderFixture.java @@ -9,6 +9,4 @@ private DefaultFilterAndOrderFixture() { public static final Double DEFAULT_MIN_SCORE = 0.00; public static final Double DEFAULT_MAX_SCORE = 5.00; public static final String DEFAULT_EVALUATION_STATUS = "ALL"; - public static final String DEFAULT_SORT_BY_CREATED_AT = "DESC"; - public static final String DEFAULT_SORT_BY_SCORE = "DESC"; } diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index 149549123..2201cc1d8 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -25,6 +25,11 @@ spring: open-in-view: false mail: host: smtp.gmail.com + data: + redis: + port: 6379 + host: localhost + password: password dataloader: enable: false @@ -33,11 +38,13 @@ security: jwt: token: secret-key: test - expire-length: 1209600000 + access-expire-length: 1209600000 + refresh-expire-length: 1209600000 algorithm: HS256 cookie: - access-token-key: token + access-token-key: accessToken + refresh-token-key: refreshToken http-only: false secure: false domain: localhost