diff --git a/.github/workflows/review_submit_notification.yml b/.github/workflows/review_submit_notification.yml index d594ccc6..826e273b 100644 --- a/.github/workflows/review_submit_notification.yml +++ b/.github/workflows/review_submit_notification.yml @@ -50,7 +50,7 @@ jobs: - name: Send Slack notification uses: slackapi/slack-github-action@v1.22.0 with: - channel-id: ${{secrets.SLACK_FE_REVIEW_CHANNEL}} + channel-id: ${{secrets.SLACK_BE_REVIEW_CHANNEL}} payload: | { "text": "리뷰가 도착했어요✉️", diff --git a/backend/src/main/java/site/coduo/common/infrastructure/swagger/config/SwaggerConfig.java b/backend/src/main/java/site/coduo/common/infrastructure/swagger/config/SwaggerConfig.java index 3149102d..e3a08213 100644 --- a/backend/src/main/java/site/coduo/common/infrastructure/swagger/config/SwaggerConfig.java +++ b/backend/src/main/java/site/coduo/common/infrastructure/swagger/config/SwaggerConfig.java @@ -9,6 +9,7 @@ import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; @Configuration public class SwaggerConfig { @@ -18,6 +19,7 @@ public OpenAPI openAPI() { return new OpenAPI() .info(apiInfo()) .components(securitySchemeComponents()) + .addServersItem(serverItem()) .addSecurityItem(securityRequirement()); } @@ -39,6 +41,12 @@ private Components securitySchemeComponents() { return new Components().addSecuritySchemes(HttpHeaders.AUTHORIZATION, bearerAuth); } + private Server serverItem() { + return new Server() + .url("https://coduo.site") + .description("클라우드에 배포된 서버"); + } + private SecurityRequirement securityRequirement() { return new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION); } diff --git a/backend/src/main/java/site/coduo/member/client/GithubApiClient.java b/backend/src/main/java/site/coduo/member/client/GithubApiClient.java index f2272030..354ffb38 100644 --- a/backend/src/main/java/site/coduo/member/client/GithubApiClient.java +++ b/backend/src/main/java/site/coduo/member/client/GithubApiClient.java @@ -1,13 +1,16 @@ package site.coduo.member.client; +import org.springframework.http.HttpStatusCode; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.ResponseSpec.ErrorHandler; import lombok.extern.slf4j.Slf4j; import site.coduo.member.client.dto.GithubUserRequest; import site.coduo.member.client.dto.GithubUserResponse; +import site.coduo.member.exception.ExternalApiCallException; @Slf4j @Component @@ -30,6 +33,10 @@ public GithubApiClient() { .build(); } + GithubApiClient(final RestClient restClient) { + this.client = restClient; + } + public GithubUserResponse getUser(final GithubUserRequest request) { return client.get() @@ -37,6 +44,18 @@ public GithubUserResponse getUser(final GithubUserRequest request) { .accept() .headers(httpHeaders -> httpHeaders.addAll(request.getHeaders())) .retrieve() + .onStatus(HttpStatusCode::isError, getErrorHandler()) .body(GithubUserResponse.class); } + + private ErrorHandler getErrorHandler() { + return (request, response) -> { + if (response.getStatusCode().is4xxClientError()) { + throw new ExternalApiCallException("Github API 호출에 실패했습니다."); + } + if (response.getStatusCode().is5xxServerError()) { + throw new ExternalApiCallException("Github API 호출 과정 중 서버 내부에서 오류가 발생했습니다."); + } + }; + } } diff --git a/backend/src/main/java/site/coduo/member/client/GithubOAuthClient.java b/backend/src/main/java/site/coduo/member/client/GithubOAuthClient.java index 2b8e6d03..985ac3f0 100644 --- a/backend/src/main/java/site/coduo/member/client/GithubOAuthClient.java +++ b/backend/src/main/java/site/coduo/member/client/GithubOAuthClient.java @@ -2,14 +2,17 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.ResponseSpec.ErrorHandler; import site.coduo.member.client.dto.TokenRequest; import site.coduo.member.client.dto.TokenResponse; +import site.coduo.member.exception.ExternalApiCallException; import site.coduo.member.infrastructure.http.Basic; @Component @@ -50,6 +53,7 @@ public TokenResponse grant(final TokenRequest request) { .header(HttpHeaders.AUTHORIZATION, new Basic(oAuthClientId, oAuthClientSecret).getValue()) .body(request.toQueryParams()) .retrieve() + .onStatus(HttpStatusCode::isError, getErrorHandler()) .body(TokenResponse.class); } @@ -60,4 +64,15 @@ public String getOAuthClientId() { public String getOAuthRedirectUri() { return oAuthRedirectUri; } + + private ErrorHandler getErrorHandler() { + return (request, response) -> { + if (response.getStatusCode().is4xxClientError()) { + throw new ExternalApiCallException("Github OAuth API 호출에 실패했습니다."); + } + if (response.getStatusCode().is5xxServerError()) { + throw new ExternalApiCallException("Github OAuth 과정 중 서버 내부에서 오류가 발생했습니다."); + } + }; + } } diff --git a/backend/src/main/java/site/coduo/member/controller/MemberController.java b/backend/src/main/java/site/coduo/member/controller/MemberController.java index e846b043..e0cafb98 100644 --- a/backend/src/main/java/site/coduo/member/controller/MemberController.java +++ b/backend/src/main/java/site/coduo/member/controller/MemberController.java @@ -4,6 +4,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -19,11 +20,17 @@ public class MemberController implements MemberControllerDocs { private final MemberService memberService; @GetMapping("/member") - public ResponseEntity getMember( - @CookieValue(SIGN_IN_COOKIE_NAME) final String token - ) { + public ResponseEntity getMember(@CookieValue(SIGN_IN_COOKIE_NAME) final String token) { final MemberReadResponse response = memberService.findMemberNameByCredential(token); return ResponseEntity.ok(response); } + + @DeleteMapping("/member") + public ResponseEntity deleteMember(@CookieValue(SIGN_IN_COOKIE_NAME) final String token) { + memberService.deleteMember(token); + + return ResponseEntity.noContent() + .build(); + } } diff --git a/backend/src/main/java/site/coduo/member/controller/MemberExceptionHandler.java b/backend/src/main/java/site/coduo/member/controller/MemberExceptionHandler.java index 0f488d75..d0a24459 100644 --- a/backend/src/main/java/site/coduo/member/controller/MemberExceptionHandler.java +++ b/backend/src/main/java/site/coduo/member/controller/MemberExceptionHandler.java @@ -9,6 +9,7 @@ import lombok.extern.slf4j.Slf4j; import site.coduo.common.controller.response.ApiErrorResponse; import site.coduo.member.controller.error.MemberApiError; +import site.coduo.member.exception.ExternalApiCallException; import site.coduo.member.exception.MemberNotFoundException; @Slf4j @@ -23,4 +24,12 @@ public ResponseEntity handlerMemberNotFoundException(final Mem return ResponseEntity.status(MemberApiError.MEMBER_NOT_FOUND_ERROR.getHttpStatus()) .body(new ApiErrorResponse(MemberApiError.MEMBER_NOT_FOUND_ERROR.getMessage())); } + + @ExceptionHandler(ExternalApiCallException.class) + public ResponseEntity handlerExternalApiCallFailureException(final ExternalApiCallException e) { + log.error(e.getMessage()); + + return ResponseEntity.status(MemberApiError.API_CALL_FAILURE_ERROR.getHttpStatus()) + .body(new ApiErrorResponse(MemberApiError.MEMBER_NOT_FOUND_ERROR.getMessage())); + } } diff --git a/backend/src/main/java/site/coduo/member/controller/docs/MemberControllerDocs.java b/backend/src/main/java/site/coduo/member/controller/docs/MemberControllerDocs.java index ef0150df..de75df8f 100644 --- a/backend/src/main/java/site/coduo/member/controller/docs/MemberControllerDocs.java +++ b/backend/src/main/java/site/coduo/member/controller/docs/MemberControllerDocs.java @@ -29,4 +29,14 @@ ResponseEntity getMember( schema = @Schema(type = "string") ) String token); + + @ApiResponse(responseCode = "204", description = "회원 정보를 삭제한다.", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE)) + ResponseEntity deleteMember( + @Parameter( + in = ParameterIn.COOKIE, + name = "coduo_whoami", + description = "사용자가 인증에 성공하면 서버에서 발급하는 쿠키", + schema = @Schema(type = "string") + ) String token); } diff --git a/backend/src/main/java/site/coduo/member/controller/error/MemberApiError.java b/backend/src/main/java/site/coduo/member/controller/error/MemberApiError.java index a2a360f7..8e0cedc2 100644 --- a/backend/src/main/java/site/coduo/member/controller/error/MemberApiError.java +++ b/backend/src/main/java/site/coduo/member/controller/error/MemberApiError.java @@ -10,7 +10,8 @@ public enum MemberApiError { AUTHENTICATION_ERROR(HttpStatus.UNAUTHORIZED, "인증되지 않은 접근입니다."), - MEMBER_NOT_FOUND_ERROR(HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다."); + MEMBER_NOT_FOUND_ERROR(HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다."), + API_CALL_FAILURE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "외부 API와 상호작용 중 실패했습니다."); private final HttpStatus httpStatus; private final String message; diff --git a/backend/src/main/java/site/coduo/member/domain/Member.java b/backend/src/main/java/site/coduo/member/domain/Member.java index fe3888c3..81bbb23a 100644 --- a/backend/src/main/java/site/coduo/member/domain/Member.java +++ b/backend/src/main/java/site/coduo/member/domain/Member.java @@ -1,5 +1,6 @@ package site.coduo.member.domain; +import java.time.LocalDateTime; import java.util.Objects; import jakarta.persistence.Column; @@ -9,6 +10,8 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; +import org.springframework.format.annotation.DateTimeFormat; + import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -41,14 +44,19 @@ public class Member extends BaseTimeEntity { @Column(name = "USER_NAME") private String username; + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + @Column(name = "DELETED_AT") + private LocalDateTime deletedAt; + @Builder private Member(final String accessToken, final String loginId, final String userId, final String profileImage, - final String username) { + final String username, final LocalDateTime deletedAt) { this.accessToken = accessToken; this.loginId = loginId; this.userId = userId; this.profileImage = profileImage; this.username = username; + this.deletedAt = deletedAt; } public void update(final Member other) { @@ -57,6 +65,15 @@ public void update(final Member other) { this.userId = other.userId; this.profileImage = other.profileImage; this.username = other.username; + this.deletedAt = other.deletedAt; + } + + public void delete() { + this.deletedAt = LocalDateTime.now(); + } + + public boolean isDeleted() { + return deletedAt != null; } @Override diff --git a/backend/src/main/java/site/coduo/member/domain/repository/MemberRepository.java b/backend/src/main/java/site/coduo/member/domain/repository/MemberRepository.java index 0e1f72ea..af500af5 100644 --- a/backend/src/main/java/site/coduo/member/domain/repository/MemberRepository.java +++ b/backend/src/main/java/site/coduo/member/domain/repository/MemberRepository.java @@ -1,15 +1,33 @@ package site.coduo.member.domain.repository; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import site.coduo.member.domain.Member; +import site.coduo.member.exception.MemberNotFoundException; public interface MemberRepository extends JpaRepository { Optional findByUserId(String userId); + List findByDeletedAtIsNull(); + + @Override + default List findAll() { + return findByDeletedAtIsNull(); + } + + default Member fetchByUserId(final String userId) { + final Member member = findByUserId(userId) + .orElseThrow(() -> new MemberNotFoundException(String.format("%s는 찾을 수 없는 회원 아이디입니다.", userId))); + if (member.isDeleted()) { + throw new MemberNotFoundException(String.format("%s는 삭제된 회원입니다.", userId)); + } + return member; + } + boolean existsByUserId(String userId); } diff --git a/backend/src/main/java/site/coduo/member/exception/ExternalApiCallException.java b/backend/src/main/java/site/coduo/member/exception/ExternalApiCallException.java new file mode 100644 index 00000000..2a8554c7 --- /dev/null +++ b/backend/src/main/java/site/coduo/member/exception/ExternalApiCallException.java @@ -0,0 +1,8 @@ +package site.coduo.member.exception; + +public class ExternalApiCallException extends MemberException { + + public ExternalApiCallException(final String message) { + super(message); + } +} diff --git a/backend/src/main/java/site/coduo/member/service/MemberService.java b/backend/src/main/java/site/coduo/member/service/MemberService.java index 1212f648..f56b345b 100644 --- a/backend/src/main/java/site/coduo/member/service/MemberService.java +++ b/backend/src/main/java/site/coduo/member/service/MemberService.java @@ -10,7 +10,6 @@ import site.coduo.member.client.dto.GithubUserResponse; import site.coduo.member.domain.Member; import site.coduo.member.domain.repository.MemberRepository; -import site.coduo.member.exception.MemberNotFoundException; import site.coduo.member.infrastructure.http.Bearer; import site.coduo.member.infrastructure.security.JwtProvider; import site.coduo.member.service.dto.member.MemberReadResponse; @@ -36,15 +35,22 @@ public void createMember(final String username, final String encryptedAccessToke public MemberReadResponse findMemberNameByCredential(final String token) { final String userId = jwtProvider.extractSubject(token); - final Member member = memberRepository.findByUserId(userId) - .orElseThrow(() -> new MemberNotFoundException(String.format("%s는 찾을 수 없는 회원 아이디입니다.", userId))); + final Member member = memberRepository.fetchByUserId(userId); return new MemberReadResponse(member.getUsername()); } public Member findMemberByCredential(final String token) { final String userId = jwtProvider.extractSubject(token); - return memberRepository.findByUserId(userId) - .orElseThrow(() -> new MemberNotFoundException(String.format("%s는 찾을 수 없는 회원 아이디입니다.", userId))); + + return memberRepository.fetchByUserId(userId); + } + + @Transactional + public void deleteMember(final String token) { + final String userId = jwtProvider.extractSubject(token); + final Member member = memberRepository.fetchByUserId(userId); + + member.delete(); } } diff --git a/backend/src/main/java/site/coduo/pairroom/controller/PairRoomController.java b/backend/src/main/java/site/coduo/pairroom/controller/PairRoomController.java index c27bc620..3d953dcb 100644 --- a/backend/src/main/java/site/coduo/pairroom/controller/PairRoomController.java +++ b/backend/src/main/java/site/coduo/pairroom/controller/PairRoomController.java @@ -9,6 +9,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -92,4 +93,10 @@ public ResponseEntity pairRoomExists(@RequestParam("acces return ResponseEntity.ok(response); } + + @DeleteMapping("/pair-room/{accessCode}") + public ResponseEntity deletePairRoom(@PathVariable("accessCode") final String accessCode) { + pairRoomService.deletePairRoom(accessCode); + return ResponseEntity.noContent().build(); + } } diff --git a/backend/src/main/java/site/coduo/pairroom/controller/docs/PairRoomDocs.java b/backend/src/main/java/site/coduo/pairroom/controller/docs/PairRoomDocs.java index f2727b27..af71bc5e 100644 --- a/backend/src/main/java/site/coduo/pairroom/controller/docs/PairRoomDocs.java +++ b/backend/src/main/java/site/coduo/pairroom/controller/docs/PairRoomDocs.java @@ -35,7 +35,7 @@ ResponseEntity getPairRoom( @ApiResponse(responseCode = "201", description = "페어룸 저장 성공", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = PairRoomCreateResponse.class))) ResponseEntity createPairRoom( - @Parameter(description = "페어 프로그래밍에 참여하는 드라이버 이름, 내비게이터 이름, 타이머 시간, 타이머 남은 시간", required = true) + @Parameter(description = "페어 프로그래밍에 참여하는 드라이버 이름, 내비게이터 이름, 타이머 시간, 타이머 남은 시간, 미션 리포지토리 링크", required = true) PairRoomCreateRequest pairRoomCreateRequest, @Parameter(description = "로그인 유저 토큰") String token @@ -74,4 +74,11 @@ ResponseEntity> getPairRooms( @ApiResponse(responseCode = "200", description = "페어룸 존재 여부", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = PairRoomExistResponse.class))) ResponseEntity pairRoomExists(String accessCode); + + @Operation(summary = "페어룸을 삭제한다.") + @ApiResponse(responseCode = "204", description = "페어룸 삭제 성공") + ResponseEntity deletePairRoom( + @Parameter(description = "페어룸 접근 코드", required = true) + String accessCode + ); } diff --git a/backend/src/main/java/site/coduo/pairroom/domain/MissionUrl.java b/backend/src/main/java/site/coduo/pairroom/domain/MissionUrl.java new file mode 100644 index 00000000..214221d6 --- /dev/null +++ b/backend/src/main/java/site/coduo/pairroom/domain/MissionUrl.java @@ -0,0 +1,13 @@ +package site.coduo.pairroom.domain; + +import lombok.Getter; + +@Getter +public class MissionUrl { + + private final String value; + + public MissionUrl(final String value) { + this.value = value; + } +} diff --git a/backend/src/main/java/site/coduo/pairroom/domain/PairRoom.java b/backend/src/main/java/site/coduo/pairroom/domain/PairRoom.java index a73ba084..95db9853 100644 --- a/backend/src/main/java/site/coduo/pairroom/domain/PairRoom.java +++ b/backend/src/main/java/site/coduo/pairroom/domain/PairRoom.java @@ -12,6 +12,7 @@ public class PairRoom { private final PairRoomStatus status; private final Pair pair; + private final MissionUrl missionUrl; private final AccessCode accessCode; public String getAccessCodeText() { @@ -26,22 +27,25 @@ public String getDriverName() { return pair.getDriverName(); } + public String getMissionUrl() { + return missionUrl.getValue(); + } + @Override public boolean equals(final Object o) { if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { + if (!(o instanceof final PairRoom pairRoom)) { return false; } - final PairRoom pairRoom = (PairRoom) o; return status == pairRoom.status && Objects.equals(pair, pairRoom.pair) && Objects.equals( - accessCode, pairRoom.accessCode); + missionUrl, pairRoom.missionUrl) && Objects.equals(accessCode, pairRoom.accessCode); } @Override public int hashCode() { - return Objects.hash(status, pair, accessCode); + return Objects.hash(status, pair, missionUrl, accessCode); } @Override @@ -49,6 +53,7 @@ public String toString() { return "PairRoom{" + "status=" + status + ", pair=" + pair + + ", missionUrl=" + missionUrl + ", accessCode=" + accessCode + '}'; } diff --git a/backend/src/main/java/site/coduo/pairroom/domain/PairRoomStatus.java b/backend/src/main/java/site/coduo/pairroom/domain/PairRoomStatus.java index 7500dc46..06a6d5b4 100644 --- a/backend/src/main/java/site/coduo/pairroom/domain/PairRoomStatus.java +++ b/backend/src/main/java/site/coduo/pairroom/domain/PairRoomStatus.java @@ -12,7 +12,8 @@ public enum PairRoomStatus { IN_PROGRESS, - COMPLETED; + COMPLETED, + DELETED; private static final Map STATUS = Arrays.stream(values()) .collect(Collectors.toMap(PairRoomStatus::name, Function.identity())); diff --git a/backend/src/main/java/site/coduo/pairroom/exception/DeletePairRoomException.java b/backend/src/main/java/site/coduo/pairroom/exception/DeletePairRoomException.java new file mode 100644 index 00000000..a470d3ed --- /dev/null +++ b/backend/src/main/java/site/coduo/pairroom/exception/DeletePairRoomException.java @@ -0,0 +1,8 @@ +package site.coduo.pairroom.exception; + +public class DeletePairRoomException extends PairRoomException { + + public DeletePairRoomException(final String message) { + super(message); + } +} diff --git a/backend/src/main/java/site/coduo/pairroom/repository/PairRoomEntity.java b/backend/src/main/java/site/coduo/pairroom/repository/PairRoomEntity.java index fa2252c5..55ca232c 100644 --- a/backend/src/main/java/site/coduo/pairroom/repository/PairRoomEntity.java +++ b/backend/src/main/java/site/coduo/pairroom/repository/PairRoomEntity.java @@ -16,6 +16,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import site.coduo.common.infrastructure.audit.entity.BaseTimeEntity; +import site.coduo.pairroom.domain.MissionUrl; import site.coduo.pairroom.domain.Pair; import site.coduo.pairroom.domain.PairName; import site.coduo.pairroom.domain.PairRoom; @@ -43,16 +44,24 @@ public class PairRoomEntity extends BaseTimeEntity { @Column(name = "DRIVER", nullable = false) private String driver; + @Column(name = "MISSION_URL", nullable = false) + private String missionUrl; + @Column(name = "ACCESS_CODE", nullable = false, unique = true) private String accessCode; @Builder - private PairRoomEntity(final Long id, final PairRoomStatus status, final String navigator, final String driver, + private PairRoomEntity(final Long id, + final PairRoomStatus status, + final String navigator, + final String driver, + final String missionUrl, final String accessCode) { this.id = id; this.status = status; this.navigator = navigator; this.driver = driver; + this.missionUrl = missionUrl; this.accessCode = accessCode; } @@ -62,6 +71,7 @@ public static PairRoomEntity from(final PairRoom pairRoom) { pairRoom.getStatus(), pairRoom.getNavigatorName(), pairRoom.getDriverName(), + pairRoom.getMissionUrl(), pairRoom.getAccessCodeText() ); } @@ -70,6 +80,7 @@ public PairRoom toDomain() { return new PairRoom( status, new Pair(new PairName(navigator), new PairName(driver)), + new MissionUrl(missionUrl), new AccessCode(accessCode) ); } @@ -84,6 +95,10 @@ public void swapNavigatorWithDriver() { this.driver = temp; } + public boolean isDelete() { + return status == PairRoomStatus.DELETED; + } + @Override public boolean equals(final Object o) { if (this == o) { @@ -104,11 +119,12 @@ public int hashCode() { @Override public String toString() { return "PairRoomEntity{" + - "id=" + id + - ", status=" + status + - ", navigator='" + navigator + '\'' + - ", driver='" + driver + '\'' + - ", accessCode='" + accessCode + '\'' + - '}'; + "id=" + id + + ", status=" + status + + ", navigator='" + navigator + '\'' + + ", driver='" + driver + '\'' + + ", missionUrl='" + missionUrl + '\'' + + ", accessCode='" + accessCode + '\'' + + '}'; } } diff --git a/backend/src/main/java/site/coduo/pairroom/repository/PairRoomRepository.java b/backend/src/main/java/site/coduo/pairroom/repository/PairRoomRepository.java index 9ca969ba..9f1bec18 100644 --- a/backend/src/main/java/site/coduo/pairroom/repository/PairRoomRepository.java +++ b/backend/src/main/java/site/coduo/pairroom/repository/PairRoomRepository.java @@ -4,6 +4,7 @@ import org.springframework.data.jpa.repository.JpaRepository; +import site.coduo.pairroom.domain.PairRoomStatus; import site.coduo.pairroom.domain.accesscode.AccessCode; import site.coduo.pairroom.exception.PairRoomNotFoundException; @@ -22,4 +23,6 @@ default PairRoomEntity fetchByAccessCode(AccessCode accessCode) { } boolean existsByAccessCode(String generatedAccessCode); + + boolean existsByAccessCodeAndStatusNot(String accessCode, PairRoomStatus status); } diff --git a/backend/src/main/java/site/coduo/pairroom/service/PairRoomService.java b/backend/src/main/java/site/coduo/pairroom/service/PairRoomService.java index bec2afc1..b2c78353 100644 --- a/backend/src/main/java/site/coduo/pairroom/service/PairRoomService.java +++ b/backend/src/main/java/site/coduo/pairroom/service/PairRoomService.java @@ -11,12 +11,14 @@ import lombok.extern.slf4j.Slf4j; import site.coduo.member.domain.Member; import site.coduo.member.service.MemberService; +import site.coduo.pairroom.domain.MissionUrl; import site.coduo.pairroom.domain.Pair; import site.coduo.pairroom.domain.PairName; import site.coduo.pairroom.domain.PairRoom; import site.coduo.pairroom.domain.PairRoomStatus; import site.coduo.pairroom.domain.accesscode.AccessCode; import site.coduo.pairroom.domain.accesscode.UUIDAccessCodeGenerator; +import site.coduo.pairroom.exception.DeletePairRoomException; import site.coduo.pairroom.repository.PairRoomEntity; import site.coduo.pairroom.repository.PairRoomMemberEntity; import site.coduo.pairroom.repository.PairRoomMemberRepository; @@ -56,14 +58,15 @@ public String savePairRoom(final PairRoomCreateRequest request, @Nullable final } public boolean existsByAccessCode(final String accessCode) { - return pairRoomRepository.existsByAccessCode(accessCode); + return pairRoomRepository.existsByAccessCodeAndStatusNot(accessCode, PairRoomStatus.DELETED); } private PairRoom createPairRoom(final PairRoomCreateRequest request) { final AccessCode accessCode = generateAccessCode(); final PairRoomStatus status = PairRoomStatus.findByName(request.status()); final Pair pair = new Pair(new PairName(request.navigator()), new PairName(request.driver())); - return new PairRoom(status, pair, accessCode); + final MissionUrl missionUrl = new MissionUrl(request.missionUrl()); + return new PairRoom(status, pair, missionUrl, accessCode); } private AccessCode generateAccessCode() { @@ -77,18 +80,27 @@ private AccessCode generateAccessCode() { @Transactional public void updateNavigatorWithDriver(final String accessCode) { final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + checkDeletePairRoom(pairRoomEntity); pairRoomEntity.swapNavigatorWithDriver(); } + private void checkDeletePairRoom(final PairRoomEntity pairRoomEntity) { + if (pairRoomEntity.isDelete()) { + throw new DeletePairRoomException("삭제된 페어룸입니다."); + } + } + @Transactional public void updatePairRoomStatus(final String accessCode, final String statusName) { + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + checkDeletePairRoom(pairRoomEntity); final PairRoomStatus status = PairRoomStatus.findByName(statusName); - final PairRoomEntity entity = pairRoomRepository.fetchByAccessCode(accessCode); - entity.updateStatus(status); + pairRoomEntity.updateStatus(status); } public PairRoomReadResponse findPairRoomAndTimer(final String accessCode) { final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + checkDeletePairRoom(pairRoomEntity); final TimerEntity timerEntity = timerRepository.fetchTimerByPairRoomEntity(pairRoomEntity); return PairRoomReadResponse.of(pairRoomEntity.toDomain(), timerEntity.toDomain()); } @@ -97,10 +109,20 @@ public List findPairRooms(final String token) { final Member member = memberService.findMemberByCredential(token); final List pairRooms = pairRoomMemberRepository.findByMember(member); - - return pairRooms.stream() + final List pairRoomEntities = pairRooms.stream() .map(PairRoomMemberEntity::getPairRoom) + .filter(pairRoomEntity -> !pairRoomEntity.isDelete()) + .toList(); + + return pairRoomEntities.stream() .map(PairRoomMemberResponse::from) .toList(); } + + @Transactional + public void deletePairRoom(final String accessCode) { + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + checkDeletePairRoom(pairRoomEntity); + pairRoomEntity.updateStatus(PairRoomStatus.DELETED); + } } diff --git a/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomCreateRequest.java b/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomCreateRequest.java index 52d0b189..68ac00b8 100644 --- a/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomCreateRequest.java +++ b/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomCreateRequest.java @@ -2,6 +2,7 @@ import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import io.swagger.v3.oas.annotations.media.Schema; @@ -23,6 +24,10 @@ public record PairRoomCreateRequest( @Min(value = 1, message = "타이머 남은 시간은 0보다 커야합니다.") long timerRemainingTime, + @Schema(description = "미션 리포지토리 링크. '그냥 시작할래요'로 생성하면 빈 문자열") + @NotNull + String missionUrl, + @Schema(description = "페어룸의 상태", example = "IN_PROGRESS") @NotBlank String status diff --git a/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomReadResponse.java b/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomReadResponse.java index bc0f77d8..0b70198e 100644 --- a/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomReadResponse.java +++ b/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomReadResponse.java @@ -19,7 +19,10 @@ public record PairRoomReadResponse( long duration, @Schema(description = "타이머 남은 시간 (millisecond 기준)", example = "5000") - long remainingTime + long remainingTime, + + @Schema(description = "미션 리포지토리 링크", example = "https://github.com/coduo-missions/coduo-javascript-rps") + String missionUrl ) { public static PairRoomReadResponse of(final PairRoom pairRoom, final Timer timer) { @@ -28,7 +31,8 @@ public static PairRoomReadResponse of(final PairRoom pairRoom, final Timer timer pairRoom.getDriverName(), pairRoom.getStatus().name(), timer.getDuration(), - timer.getRemainingTime() + timer.getRemainingTime(), + pairRoom.getMissionUrl() ); } } diff --git a/backend/src/main/java/site/coduo/sync/service/SseEventStream.java b/backend/src/main/java/site/coduo/sync/service/SseEventStream.java index 7cf36a49..d026be3e 100644 --- a/backend/src/main/java/site/coduo/sync/service/SseEventStream.java +++ b/backend/src/main/java/site/coduo/sync/service/SseEventStream.java @@ -60,8 +60,7 @@ public void flush(final String name, final String message) { .name(name) .data(message) ); - } catch (final IOException e) { - log.warn("SSE 통신 중 에러가 발생했습니다."); + } catch (final IOException ignored) { } } diff --git a/backend/src/test/java/site/coduo/acceptance/CategoryAcceptanceTest.java b/backend/src/test/java/site/coduo/acceptance/CategoryAcceptanceTest.java index 5252ebda..2a14ffbb 100644 --- a/backend/src/test/java/site/coduo/acceptance/CategoryAcceptanceTest.java +++ b/backend/src/test/java/site/coduo/acceptance/CategoryAcceptanceTest.java @@ -39,7 +39,7 @@ static CategoryCreateResponse createCategory(final String accessCode, final Cate void show_category() { //given final PairRoomCreateResponse pairRoomUrl = PairRoomAcceptanceTest.createPairRoom( - new PairRoomCreateRequest("레디", "프람", 10000L, 10000L, + new PairRoomCreateRequest("레디", "프람", 10000L, 10000L, "https://missionUrl.xxx", PairRoomStatus.IN_PROGRESS.name())); createCategory(pairRoomUrl.accessCode(), new CategoryCreateRequest("새로운 카테고리")); @@ -65,14 +65,15 @@ void show_category() { void update_category() { //given final PairRoomCreateResponse pairRoomUrl = PairRoomAcceptanceTest.createPairRoom( - new PairRoomCreateRequest("레디", "프람", 10000L, 10000L, + new PairRoomCreateRequest("레디", "프람", 10000L, 10000L, "https://missionUrl.xxx", PairRoomStatus.IN_PROGRESS.name())); final CategoryCreateResponse previousCategory = createCategory(pairRoomUrl.accessCode(), new CategoryCreateRequest("이전 카테고리")); final String updateName = "변경된 카테고리"; - final CategoryUpdateRequest request = new CategoryUpdateRequest(Long.parseLong(previousCategory.id()), updateName); + final CategoryUpdateRequest request = new CategoryUpdateRequest(Long.parseLong(previousCategory.id()), + updateName); //when & then final CategoryUpdateResponse categoryUpdateResponse = RestAssured @@ -100,7 +101,7 @@ void update_category() { void delete_category() { //given final PairRoomCreateResponse pairRoomUrl = PairRoomAcceptanceTest.createPairRoom( - new PairRoomCreateRequest("레디", "프람", 10000L, 10000L, + new PairRoomCreateRequest("레디", "프람", 10000L, 10000L, "https://missionUrl.xxx", PairRoomStatus.IN_PROGRESS.name())); final CategoryCreateResponse category = createCategory(pairRoomUrl.accessCode(), diff --git a/backend/src/test/java/site/coduo/acceptance/MemberAcceptanceTest.java b/backend/src/test/java/site/coduo/acceptance/MemberAcceptanceTest.java index 254e9244..f8cffac0 100644 --- a/backend/src/test/java/site/coduo/acceptance/MemberAcceptanceTest.java +++ b/backend/src/test/java/site/coduo/acceptance/MemberAcceptanceTest.java @@ -50,19 +50,67 @@ void search_member_info() { .body("username", is(member.getUsername())); } - String login(Member member) { - final String sessionId = GithubAcceptanceTest.createAccessTokenCookie(); + @Test + @DisplayName("회원을 삭제한다.") + void delete_member() { + //given + final Member member = Member.builder() + .userId("123") + .accessToken("access") + .loginId("login") + .username("username") + .profileImage("some image") + .build(); + final String loginToken = jwtProvider.sign(member.getUserId()); memberRepository.save(member); - return RestAssured + //when && then + RestAssured .given() - .cookie("JSESSIONID", sessionId) + .cookie(SIGN_IN_COOKIE_NAME, loginToken) .when() - .get("/api/sign-in/callback") + .delete("/api/member") - .thenReturn() - .cookie("coudo_whoami"); + .then() + .statusCode(HttpStatus.SC_NO_CONTENT); + } + + @Test + @DisplayName("존재하지 않는 회원을 삭제한다.") + void delete_not_member() { + //given + final Member member = Member.builder() + .userId("123") + .accessToken("access") + .loginId("login") + .username("username") + .profileImage("some image") + .build(); + + final String loginToken = jwtProvider.sign(member.getUserId()); + memberRepository.save(member); + + //when && then + RestAssured + .given() + .cookie(SIGN_IN_COOKIE_NAME, loginToken) + + .when() + .delete("/api/member") + + .then() + .statusCode(HttpStatus.SC_NO_CONTENT); + + RestAssured + .given() + .cookie(SIGN_IN_COOKIE_NAME, loginToken) + + .when() + .delete("/api/member") + + .then() + .statusCode(HttpStatus.SC_NOT_FOUND); } } diff --git a/backend/src/test/java/site/coduo/acceptance/PairRoomAcceptanceTest.java b/backend/src/test/java/site/coduo/acceptance/PairRoomAcceptanceTest.java index 7dab4e66..f799d14b 100644 --- a/backend/src/test/java/site/coduo/acceptance/PairRoomAcceptanceTest.java +++ b/backend/src/test/java/site/coduo/acceptance/PairRoomAcceptanceTest.java @@ -37,7 +37,8 @@ static PairRoomCreateResponse createPairRoom(final PairRoomCreateRequest request void show_pair_room() { //given final PairRoomCreateResponse pairRoomUrl = - createPairRoom(new PairRoomCreateRequest("레디", "프람", 10000L, 10000L, "IN_PROGRESS")); + createPairRoom( + new PairRoomCreateRequest("레디", "프람", 10000L, 10000L, "https://missionUrl.xxx", "IN_PROGRESS")); //when & then RestAssured @@ -60,7 +61,8 @@ void show_pair_room() { void update_pair_room_status() { //given final PairRoomCreateResponse accessCode = - createPairRoom(new PairRoomCreateRequest("레디", "프람", 1000L, 100L, "IN_PROGRESS")); + createPairRoom( + new PairRoomCreateRequest("레디", "프람", 1000L, 100L, "https://missionUrl.xxx", "IN_PROGRESS")); final Map status = Map.of("status", PairRoomStatus.IN_PROGRESS.name()); // when & then @@ -85,7 +87,8 @@ void update_pair_room_status() { void update_driver_navigator() { // given final PairRoomCreateResponse accessCode = - createPairRoom(new PairRoomCreateRequest("레디", "프람", 1000L, 100L, "IN_PROGRESS")); + createPairRoom( + new PairRoomCreateRequest("레디", "프람", 1000L, 100L, "https://missionUrl.xxx", "IN_PROGRESS")); // when & then RestAssured @@ -105,7 +108,8 @@ void update_driver_navigator() { void exist_pair_room_true() { //given final PairRoomCreateResponse accessCode = - createPairRoom(new PairRoomCreateRequest("레디", "프람", 1000L, 100L, "IN_PROGRESS")); + createPairRoom( + new PairRoomCreateRequest("레디", "프람", 1000L, 100L, "https://missionUrl.xxx", "IN_PROGRESS")); // when & then final PairRoomExistResponse response = RestAssured @@ -151,4 +155,25 @@ void exist_pair_room_false() { assertThat(response.exists()).isFalse(); } + + @Test + @DisplayName("페어룸을 삭제한다.") + void delete_pair_room() { + // given + final PairRoomCreateResponse accessCode = + createPairRoom( + new PairRoomCreateRequest("레디", "프람", 1000L, 100L, "https://missionUrl.xxx", "IN_PROGRESS")); + + // when & then + RestAssured + .given() + .log() + .all() + + .when() + .delete("/api/pair-room/{access-code}", accessCode.accessCode()) + + .then() + .statusCode(204); + } } diff --git a/backend/src/test/java/site/coduo/acceptance/ReferenceAcceptanceTest.java b/backend/src/test/java/site/coduo/acceptance/ReferenceAcceptanceTest.java index 1a7d638f..2b4c2db6 100644 --- a/backend/src/test/java/site/coduo/acceptance/ReferenceAcceptanceTest.java +++ b/backend/src/test/java/site/coduo/acceptance/ReferenceAcceptanceTest.java @@ -10,7 +10,6 @@ import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.transaction.annotation.Transactional; import io.restassured.RestAssured; import io.restassured.http.ContentType; @@ -18,8 +17,8 @@ import site.coduo.pairroom.service.dto.PairRoomCreateResponse; import site.coduo.referencelink.service.dto.CategoryCreateRequest; import site.coduo.referencelink.service.dto.CategoryCreateResponse; +import site.coduo.referencelink.service.dto.ReferenceLinkResponse; -@Transactional class ReferenceAcceptanceTest extends AcceptanceFixture { @Test @@ -28,7 +27,7 @@ void reference_link_create_request() { // given final PairRoomCreateResponse pairRoom = createPairRoom(new PairRoomCreateRequest("레모네", "프람", 10000L, 10000L, - "IN_PROGRESS")); + "https://missionUrl.xxx", "IN_PROGRESS")); final CategoryCreateResponse category = CategoryAcceptanceTest.createCategory( pairRoom.accessCode(), new CategoryCreateRequest("타입스크립트")); @@ -56,7 +55,7 @@ void read_all_reference_link_request() { // given final PairRoomCreateResponse pairRoom = createPairRoom(new PairRoomCreateRequest("레모네", "프람", 10000L, 10000L, - "IN_PROGRESS")); + "https://missionUrl.xxx", "IN_PROGRESS")); createReferenceLink("http://www.some1.url", pairRoom.accessCode(), "카테고리1"); createReferenceLink("http://www.some2.url", pairRoom.accessCode(), "카테고리2"); @@ -79,7 +78,8 @@ void read_all_reference_link_request() { void read_reference_link_without_open_graph() { // given final PairRoomCreateResponse pairRoom = - createPairRoom(new PairRoomCreateRequest("잉크", "해시", 1000L, 100L, "IN_PROGRESS")); + createPairRoom( + new PairRoomCreateRequest("잉크", "해시", 1000L, 100L, "https://missionUrl.xxx", "IN_PROGRESS")); final String expectedUrl = "http://www.deleasfsdte.com"; createReferenceLink(expectedUrl, pairRoom.accessCode(), "카테고리"); @@ -101,18 +101,22 @@ void read_reference_link_without_open_graph() { .body("[0].image", is("")); } - void createReferenceLink(final String url, String accessCodeText, String categoryName) { + ReferenceLinkResponse createReferenceLink(final String url, String accessCodeText, String categoryName) { final CategoryCreateResponse response = CategoryAcceptanceTest.createCategory( accessCodeText, new CategoryCreateRequest(categoryName)); final Map request = Map.of("url", url, "categoryId", response.id()); - RestAssured + return RestAssured .given() .contentType(ContentType.JSON) .body(request) .when() - .post("/api/" + accessCodeText + "/reference-link"); + .post("/api/" + accessCodeText + "/reference-link") + + .then() + .extract() + .as(ReferenceLinkResponse.class); } @Test @@ -120,9 +124,11 @@ void createReferenceLink(final String url, String accessCodeText, String categor void delete_reference_link_request() { // given final PairRoomCreateResponse pairRoom = - createPairRoom(new PairRoomCreateRequest("레모네", "프람", 1000L, 1000L, "IN_PROGRESS")); + createPairRoom( + new PairRoomCreateRequest("레모네", "프람", 1000L, 1000L, "https://missionUrl.xxx", "IN_PROGRESS")); - createReferenceLink("http://www.delete.com", pairRoom.accessCode(), "카테고리 이름"); + final ReferenceLinkResponse response = createReferenceLink("http://www.delete.com", pairRoom.accessCode(), + "카테고리 이름"); // when & then RestAssured @@ -131,7 +137,7 @@ void delete_reference_link_request() { .when() .log().all() - .delete("/api/" + pairRoom.accessCode() + "/reference-link/1") + .delete("/api/" + pairRoom.accessCode() + "/reference-link/" + response.id()) .then() .assertThat() diff --git a/backend/src/test/java/site/coduo/acceptance/SseAcceptanceTest.java b/backend/src/test/java/site/coduo/acceptance/SseAcceptanceTest.java index 4e4543b7..09e2a881 100644 --- a/backend/src/test/java/site/coduo/acceptance/SseAcceptanceTest.java +++ b/backend/src/test/java/site/coduo/acceptance/SseAcceptanceTest.java @@ -29,7 +29,7 @@ static void createConnect(final String accessCode) { void create_sse_connection() { // given final PairRoomCreateRequest request = new PairRoomCreateRequest("프람", "레모네", 10000L, - 10000L, PairRoomStatus.IN_PROGRESS.name()); + 10000L, "https://missionUrl.xxx", PairRoomStatus.IN_PROGRESS.name()); final String accessCode = createPairRoom(request).accessCode(); // when & then @@ -54,6 +54,7 @@ void delete_sse_connection() { "잉크", 1000L, 1000L, + "https://missionUrl.xxx", PairRoomStatus.IN_PROGRESS.name() ); final String accessCode = createPairRoom(request).accessCode(); diff --git a/backend/src/test/java/site/coduo/acceptance/TimerAcceptanceTest.java b/backend/src/test/java/site/coduo/acceptance/TimerAcceptanceTest.java index 5c8b4aca..b0d3330c 100644 --- a/backend/src/test/java/site/coduo/acceptance/TimerAcceptanceTest.java +++ b/backend/src/test/java/site/coduo/acceptance/TimerAcceptanceTest.java @@ -52,6 +52,7 @@ void get_timer() { "파란", 10000L, 10000L, + "https://missionUrl.xxx", PairRoomStatus.IN_PROGRESS.name()) ); @@ -75,6 +76,7 @@ void update_timer_duration() { "파슬리", 10000L, 10000L, + "https://missionUrl.xxx", PairRoomStatus.IN_PROGRESS.name()) ); final TimerUpdateRequest request = new TimerUpdateRequest(20000L, 3000L); @@ -96,8 +98,9 @@ void update_timer_duration() { @DisplayName("타이머를 시작한다.") void start_timer() { // given - final String accessCode = createPairRoom(new PairRoomCreateRequest("fram", "lemone", 10000L, 10000L, - PairRoomStatus.IN_PROGRESS.name())); + final String accessCode = createPairRoom( + new PairRoomCreateRequest("fram", "lemone", 10000L, 10000L, "https://missionUrl.xxx", + PairRoomStatus.IN_PROGRESS.name())); createConnect(accessCode); // when & then @@ -115,8 +118,9 @@ void start_timer() { @DisplayName("타이머를 종료한다.") void stop_timer() { // given - final String accessCode = createPairRoom(new PairRoomCreateRequest("fram", "lemone", 10000L, 10000L, - PairRoomStatus.IN_PROGRESS.name())); + final String accessCode = createPairRoom( + new PairRoomCreateRequest("fram", "lemone", 10000L, 10000L, "https://missionUrl.xxx", + PairRoomStatus.IN_PROGRESS.name())); timerStart(accessCode); // when & then diff --git a/backend/src/test/java/site/coduo/fake/FakeClient.java b/backend/src/test/java/site/coduo/fake/FakeClient.java new file mode 100644 index 00000000..99e4087f --- /dev/null +++ b/backend/src/test/java/site/coduo/fake/FakeClient.java @@ -0,0 +1,456 @@ +package site.coduo.fake; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.URI; +import java.nio.charset.Charset; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.StreamingHttpOutputMessage.Body; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpRequestInitializer; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.client.observation.ClientRequestObservationConvention; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.mock.http.client.MockClientHttpRequest; +import org.springframework.mock.http.client.MockClientHttpResponse; +import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.ResponseSpec.ErrorHandler; +import org.springframework.web.util.UriBuilder; +import org.springframework.web.util.UriBuilderFactory; + +import io.micrometer.observation.ObservationRegistry; +import site.coduo.member.client.dto.GithubUserResponse; + +public class FakeClient implements RestClient { + + @Override + public RequestHeadersUriSpec get() { + return new FakeRequestHeaderUriSpec(); + } + + @Override + public RequestHeadersUriSpec head() { + return new FakeRequestHeaderUriSpec(); + } + + @Override + public RequestBodyUriSpec post() { + return new FakeRequestBodyBodyUriSpec(); + } + + @Override + public RequestBodyUriSpec put() { + return new FakeRequestBodyBodyUriSpec(); + } + + @Override + public RequestBodyUriSpec patch() { + return new FakeRequestBodyBodyUriSpec(); + } + + @Override + public RequestHeadersUriSpec delete() { + return new FakeRequestHeaderUriSpec(); + } + + @Override + public RequestHeadersUriSpec options() { + return new FakeRequestHeaderUriSpec(); + } + + @Override + public RequestBodyUriSpec method(final HttpMethod method) { + return new FakeRequestBodyBodyUriSpec(); + } + + @Override + public Builder mutate() { + return new FakeBuilder(); + } + + + class FakeBuilder implements Builder { + + @Override + public Builder baseUrl(final String baseUrl) { + return this; + } + + @Override + public Builder defaultUriVariables(final Map defaultUriVariables) { + return this; + } + + @Override + public Builder uriBuilderFactory(final UriBuilderFactory uriBuilderFactory) { + return this; + } + + @Override + public Builder defaultHeader(final String header, final String... values) { + return this; + } + + @Override + public Builder defaultHeaders(final Consumer headersConsumer) { + return this; + } + + @Override + public Builder defaultRequest(final Consumer> defaultRequest) { + return this; + } + + @Override + public Builder defaultStatusHandler(final Predicate statusPredicate, + final ErrorHandler errorHandler) { + return this; + } + + @Override + public Builder defaultStatusHandler(final ResponseErrorHandler errorHandler) { + return this; + } + + @Override + public Builder requestInterceptor(final ClientHttpRequestInterceptor interceptor) { + return this; + } + + @Override + public Builder requestInterceptors(final Consumer> interceptorsConsumer) { + return this; + } + + @Override + public Builder requestInitializer(final ClientHttpRequestInitializer initializer) { + return this; + } + + @Override + public Builder requestInitializers(final Consumer> initializersConsumer) { + return this; + } + + @Override + public Builder requestFactory(final ClientHttpRequestFactory requestFactory) { + return this; + } + + @Override + public Builder messageConverters(final Consumer>> configurer) { + return this; + } + + @Override + public Builder observationRegistry(final ObservationRegistry observationRegistry) { + return this; + } + + @Override + public Builder observationConvention(final ClientRequestObservationConvention observationConvention) { + return this; + } + + @Override + public Builder apply(final Consumer builderConsumer) { + return this; + } + + @Override + public Builder clone() { + return this; + } + + @Override + public RestClient build() { + return new FakeClient(); + } + } + + class FakeResponseSpec implements ResponseSpec { + + private final HttpRequest request; + private final ClientHttpResponse response; + private final HttpStatus httpStatus; + + public FakeResponseSpec(final HttpStatus httpStatus, final HttpMethod method, final URI url) { + this.httpStatus = httpStatus; + this.request = new MockClientHttpRequest(method, url); + this.response = new MockClientHttpResponse(new byte[]{}, httpStatus.value()); + } + + @Override + public ResponseSpec onStatus(final Predicate statusPredicate, final ErrorHandler errorHandler) { + final boolean error = statusPredicate.test(httpStatus); + if (error) { + try { + errorHandler.handle(request, response); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + return this; + } + + @Override + public ResponseSpec onStatus(final ResponseErrorHandler errorHandler) { + try { + errorHandler.handleError(response); + } catch (IOException e) { + throw new RuntimeException(e); + } + return this; + } + + @Override + public T body(final Class bodyType) { + if (bodyType.isInstance(new GithubUserResponse("", "", ""))) { + try { + return bodyType.getDeclaredConstructor(String.class, String.class, String.class) + .newInstance("userId", "login", "avatar_url"); + } catch (final NoSuchMethodException | InstantiationException | IllegalAccessException | + InvocationTargetException e) { + throw new RuntimeException(e); + } + } + return null; + } + + @Override + public T body(final ParameterizedTypeReference bodyType) { + return null; + } + + @Override + public ResponseEntity toEntity(final Class bodyType) { + return null; + } + + @Override + public ResponseEntity toEntity(final ParameterizedTypeReference bodyType) { + return null; + } + + @Override + public ResponseEntity toBodilessEntity() { + return null; + } + } + + class FakeRequestHeaderUriSpec implements RequestHeadersUriSpec { + + private final HttpHeaders headers = new HttpHeaders(); + private String endPoint; + + @Override + public RequestHeadersSpec accept(final MediaType... acceptableMediaTypes) { + Stream.of(acceptableMediaTypes) + .forEach(value -> headers.add(HttpHeaders.ACCEPT, value.toString())); + + return this; + } + + @Override + public RequestHeadersSpec acceptCharset(final Charset... acceptableCharsets) { + Stream.of(acceptableCharsets) + .forEach(value -> headers.add(HttpHeaders.ACCEPT_CHARSET, value.displayName())); + + return this; + } + + @Override + public RequestHeadersSpec ifModifiedSince(final ZonedDateTime ifModifiedSince) { + headers.add(HttpHeaders.IF_MODIFIED_SINCE, ifModifiedSince.toString()); + return this; + } + + @Override + public RequestHeadersSpec ifNoneMatch(final String... ifNoneMatches) { + Stream.of(ifNoneMatches) + .forEach(value -> headers.add(HttpHeaders.IF_NONE_MATCH, value)); + return this; + } + + @Override + public RequestHeadersSpec header(final String headerName, final String... headerValues) { + Stream.of(headerValues) + .forEach(value -> headers.add(headerName, value)); + return this; + } + + @Override + public RequestHeadersSpec headers(final Consumer consumer) { + consumer.accept(headers); + return this; + } + + @Override + public RequestHeadersSpec httpRequest(final Consumer consumer) { + return this; + } + + @Override + public ResponseSpec retrieve() { + if (endPoint.equals("/user")) { + final String token = headers.get(HttpHeaders.AUTHORIZATION).get(0).substring(7); + if (token.isBlank()) { + return new FakeResponseSpec(HttpStatus.INTERNAL_SERVER_ERROR, HttpMethod.GET, URI.create("/user")); + } + return new FakeResponseSpec(HttpStatus.OK, HttpMethod.GET, URI.create("/user")); + } + return null; + } + + @Override + public Object exchange(final ExchangeFunction exchangeFunction, final boolean close) { + return null; + } + + @Override + public RequestHeadersSpec uri(final URI uri) { + endPoint = uri.toString(); + return this; + } + + @Override + public RequestHeadersSpec uri(final String uri, final Object... uriVariables) { + endPoint = uri; + return this; + } + + @Override + public RequestHeadersSpec uri(final String uri, final Map uriVariables) { + endPoint = uri; + return this; + } + + @Override + public RequestHeadersSpec uri(final String uri, final Function function) { + endPoint = uri; + return this; + } + + @Override + public RequestHeadersSpec uri(final Function function) { + return this; + } + } + + class FakeRequestBodyBodyUriSpec implements RequestBodyUriSpec { + + @Override + public RequestBodySpec contentLength(final long contentLength) { + return this; + } + + @Override + public RequestBodySpec contentType(final MediaType contentType) { + return this; + } + + @Override + public RequestBodySpec body(final Object body) { + return this; + } + + @Override + public RequestBodySpec body(final T body, final ParameterizedTypeReference bodyType) { + return this; + } + + @Override + public RequestBodySpec body(final Body body) { + return this; + } + + @Override + public RequestBodySpec accept(final MediaType... acceptableMediaTypes) { + return this; + } + + @Override + public RequestBodySpec acceptCharset(final Charset... acceptableCharsets) { + return this; + } + + @Override + public RequestBodySpec ifModifiedSince(final ZonedDateTime ifModifiedSince) { + return this; + } + + @Override + public RequestBodySpec ifNoneMatch(final String... ifNoneMatches) { + return this; + } + + @Override + public RequestBodySpec header(final String headerName, final String... headerValues) { + return this; + } + + @Override + public RequestBodySpec headers(final Consumer headersConsumer) { + return this; + } + + @Override + public RequestBodySpec httpRequest(final Consumer requestConsumer) { + return this; + } + + @Override + public ResponseSpec retrieve() { + return null; + } + + @Override + public T exchange(final ExchangeFunction exchangeFunction, final boolean close) { + return null; + } + + @Override + public RequestBodySpec uri(final URI uri) { + return this; + } + + @Override + public RequestBodySpec uri(final String uri, final Object... uriVariables) { + return this; + } + + @Override + public RequestBodySpec uri(final String uri, final Map uriVariables) { + return this; + } + + @Override + public RequestBodySpec uri(final String uri, final Function uriFunction) { + return this; + } + + @Override + public RequestBodySpec uri(final Function uriFunction) { + return this; + } + } + +} diff --git a/backend/src/test/java/site/coduo/fixture/PairRoomFixture.java b/backend/src/test/java/site/coduo/fixture/PairRoomFixture.java index 00fa062c..c214b14f 100644 --- a/backend/src/test/java/site/coduo/fixture/PairRoomFixture.java +++ b/backend/src/test/java/site/coduo/fixture/PairRoomFixture.java @@ -4,6 +4,7 @@ import static site.coduo.fixture.AccessCodeFixture.ALPHABET_ACCESS_CODE; import static site.coduo.fixture.AccessCodeFixture.NUMBER_ACCESS_CODE; +import site.coduo.pairroom.domain.MissionUrl; import site.coduo.pairroom.domain.Pair; import site.coduo.pairroom.domain.PairName; import site.coduo.pairroom.domain.PairRoom; @@ -17,6 +18,7 @@ public class PairRoomFixture { new PairName("잉크"), new PairName("레디") ), + new MissionUrl("https://github.com/coduo-missions/coduo-javascript-rps"), ACCESS_CODE); public static final PairRoom FRAM_LEMONE_ROOM = new PairRoom( @@ -25,6 +27,7 @@ public class PairRoomFixture { new PairName("프람"), new PairName("레모네") ), + new MissionUrl("https://github.com/coduo-missions/coduo-javascript-rps"), ALPHABET_ACCESS_CODE); public static final PairRoom KELY_LEMONE_ROOM = new PairRoom( @@ -33,6 +36,7 @@ public class PairRoomFixture { new PairName("켈리"), new PairName("레모네") ), + new MissionUrl("https://github.com/coduo-missions/coduo-javascript-rps"), NUMBER_ACCESS_CODE ); } diff --git a/backend/src/test/java/site/coduo/member/client/GithubApiClientTest.java b/backend/src/test/java/site/coduo/member/client/GithubApiClientTest.java new file mode 100644 index 00000000..5ad40b70 --- /dev/null +++ b/backend/src/test/java/site/coduo/member/client/GithubApiClientTest.java @@ -0,0 +1,45 @@ +package site.coduo.member.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import site.coduo.fake.FakeClient; +import site.coduo.member.client.dto.GithubUserRequest; +import site.coduo.member.client.dto.GithubUserResponse; +import site.coduo.member.exception.ExternalApiCallException; +import site.coduo.member.infrastructure.http.Bearer; + +class GithubApiClientTest { + + + @Test + @DisplayName("Github로부터 회원정보를 불러온다.") + void get_member_from_github() { + // given + final FakeClient client = new FakeClient(); + final GithubApiClient githubApiClient = new GithubApiClient(client); + final GithubUserRequest request = new GithubUserRequest(new Bearer("ok")); + + // when + final GithubUserResponse response = githubApiClient.getUser(request); + + // then + assertThat(response).isEqualTo(new GithubUserResponse("userId", "login", "avatar_url")); + } + + @Test + @DisplayName("토큰값이 잘못된 경우 예외 발생한다.") + void throw_exception_when_token_is_invalid() { + // given + final FakeClient client = new FakeClient(); + final GithubApiClient githubApiClient = new GithubApiClient(client); + final GithubUserRequest request = new GithubUserRequest(new Bearer("")); + + // when & then + assertThatThrownBy(() -> githubApiClient.getUser(request)) + .isInstanceOf(ExternalApiCallException.class); + } +} diff --git a/backend/src/test/java/site/coduo/member/service/MemberServiceTest.java b/backend/src/test/java/site/coduo/member/service/MemberServiceTest.java index 4dddba76..ef8eead9 100644 --- a/backend/src/test/java/site/coduo/member/service/MemberServiceTest.java +++ b/backend/src/test/java/site/coduo/member/service/MemberServiceTest.java @@ -2,6 +2,8 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.List; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -89,4 +91,28 @@ void search_member_by_login_token() { // then assertThat(findMember.getUsername()).isEqualTo(member.getUsername()); } + + @Test + @DisplayName("회원을 삭제한다.") + void delete_member() { + // given + final Member member = Member.builder() + .userId("userid") + .accessToken("access") + .loginId("login") + .username("username") + .profileImage("some image") + .build(); + final String token = jwtProvider.sign(member.getUserId()); + + memberRepository.save(member); + final List beforeDelete = memberRepository.findAll(); + + // when + memberService.deleteMember(token); + + //then + final List afterDelete = memberRepository.findAll(); + assertThat(afterDelete).hasSize(beforeDelete.size() - 1); + } } diff --git a/backend/src/test/java/site/coduo/pairroom/domain/PairRoomEntityTest.java b/backend/src/test/java/site/coduo/pairroom/domain/PairRoomEntityTest.java index d3771592..69818feb 100644 --- a/backend/src/test/java/site/coduo/pairroom/domain/PairRoomEntityTest.java +++ b/backend/src/test/java/site/coduo/pairroom/domain/PairRoomEntityTest.java @@ -21,9 +21,10 @@ void create_pair_room() { final String secondName = "second"; final Pair pair = new Pair(new PairName(firstName), new PairName(secondName)); final PairRoomStatus pairRoomStatus = PairRoomStatus.IN_PROGRESS; + final MissionUrl missionUrl = new MissionUrl("https://missionUrl.xxx"); // when & then - assertThatCode(() -> new PairRoom(pairRoomStatus, pair, ACCESS_CODE)) + assertThatCode(() -> new PairRoom(pairRoomStatus, pair, missionUrl, ACCESS_CODE)) .doesNotThrowAnyException(); } @@ -34,6 +35,7 @@ void change_nav_and_driver() { final PairRoomEntity sut = PairRoomEntity.from( new PairRoom(PairRoomStatus.IN_PROGRESS, new Pair(new PairName("navi"), new PairName("dri")), + new MissionUrl("https://missionUrl.xxx"), new AccessCode("access")) ); @@ -45,4 +47,40 @@ void change_nav_and_driver() { .extracting("navigator", "driver") .contains("dri", "navi"); } + + @Test + @DisplayName("페어룸 상태가 DELETE면 true를 반환한다.") + void pairRoomEntityStatusIsDelete() { + // Given + final PairRoomEntity sut = PairRoomEntity.from( + new PairRoom(PairRoomStatus.DELETED, + new Pair(new PairName("navi"), new PairName("dri")), + new MissionUrl("https://missionUrl.xxx"), + new AccessCode("access")) + ); + + // When + final boolean isDelete = sut.isDelete(); + + // Then + assertThat(isDelete).isTrue(); + } + + @Test + @DisplayName("페어룸 상태가 DELETE가 아니면 false를 반환한다.") + void pairRoomEntityStatusIsNotDelete() { + // Given + final PairRoomEntity sut = PairRoomEntity.from( + new PairRoom(PairRoomStatus.IN_PROGRESS, + new Pair(new PairName("navi"), new PairName("dri")), + new MissionUrl("https://missionUrl.xxx"), + new AccessCode("access")) + ); + + // When + final boolean isDelete = sut.isDelete(); + + // Then + assertThat(isDelete).isFalse(); + } } diff --git a/backend/src/test/java/site/coduo/pairroom/repository/PairRoomEntityRepositoryTest.java b/backend/src/test/java/site/coduo/pairroom/repository/PairRoomEntityRepositoryTest.java index 76bf53e8..49d6ee15 100644 --- a/backend/src/test/java/site/coduo/pairroom/repository/PairRoomEntityRepositoryTest.java +++ b/backend/src/test/java/site/coduo/pairroom/repository/PairRoomEntityRepositoryTest.java @@ -10,6 +10,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; +import site.coduo.pairroom.domain.MissionUrl; import site.coduo.pairroom.domain.Pair; import site.coduo.pairroom.domain.PairName; import site.coduo.pairroom.domain.PairRoom; @@ -28,9 +29,9 @@ class PairRoomEntityRepositoryTest { void search_persistence_by_access_code_exists_case() { // given final Pair pair = new Pair(new PairName("hello"), new PairName("world")); - final PairRoom pairRoom = new PairRoom(PairRoomStatus.IN_PROGRESS, pair, new AccessCode("code")); - final PairRoomEntity entity = PairRoomEntity.from( - pairRoom); + final MissionUrl missionUrl = new MissionUrl("https://missionUrl.xxx"); + final PairRoom pairRoom = new PairRoom(PairRoomStatus.IN_PROGRESS, pair, missionUrl, new AccessCode("code")); + final PairRoomEntity entity = PairRoomEntity.from(pairRoom); pairRoomRepository.save(entity); // when @@ -46,7 +47,8 @@ void search_persistence_by_access_code_exists_case() { void search_persistence_by_access_code_not_exists_case() { // given final Pair pair = new Pair(new PairName("hello"), new PairName("world")); - final PairRoom pairRoom = new PairRoom(PairRoomStatus.IN_PROGRESS, pair, new AccessCode("code")); + final MissionUrl missionUrl = new MissionUrl("https://missionUrl.xxx"); + final PairRoom pairRoom = new PairRoom(PairRoomStatus.IN_PROGRESS, pair, missionUrl, new AccessCode("code")); // when final Optional persistence = pairRoomRepository.findByAccessCode( @@ -62,7 +64,8 @@ void search_persistence_by_access_code_domain_exists_case() { // given final Pair pair = new Pair(new PairName("hello"), new PairName("world")); final AccessCode code = new AccessCode("code"); - final PairRoom pairRoom = new PairRoom(PairRoomStatus.IN_PROGRESS, pair, code); + final MissionUrl missionUrl = new MissionUrl("https://missionUrl.xxx"); + final PairRoom pairRoom = new PairRoom(PairRoomStatus.IN_PROGRESS, pair, missionUrl, code); pairRoomRepository.save(PairRoomEntity.from(pairRoom)); // when diff --git a/backend/src/test/java/site/coduo/pairroom/service/PairRoomServiceTest.java b/backend/src/test/java/site/coduo/pairroom/service/PairRoomServiceTest.java index b02c5070..62a5939f 100644 --- a/backend/src/test/java/site/coduo/pairroom/service/PairRoomServiceTest.java +++ b/backend/src/test/java/site/coduo/pairroom/service/PairRoomServiceTest.java @@ -17,11 +17,13 @@ import site.coduo.member.domain.Member; import site.coduo.member.domain.repository.MemberRepository; import site.coduo.member.infrastructure.security.JwtProvider; +import site.coduo.pairroom.domain.MissionUrl; import site.coduo.pairroom.domain.Pair; import site.coduo.pairroom.domain.PairName; import site.coduo.pairroom.domain.PairRoom; import site.coduo.pairroom.domain.PairRoomStatus; import site.coduo.pairroom.domain.accesscode.AccessCode; +import site.coduo.pairroom.exception.DeletePairRoomException; import site.coduo.pairroom.exception.PairRoomNotFoundException; import site.coduo.pairroom.repository.PairRoomEntity; import site.coduo.pairroom.repository.PairRoomRepository; @@ -52,7 +54,7 @@ class PairRoomServiceTest { void create_pair_room() { // given final PairRoomCreateRequest request = - new PairRoomCreateRequest("레디", "프람", 1000L, 100L, + new PairRoomCreateRequest("레디", "프람", 1000L, 100L, "https://missionUrl.xxx", PairRoomStatus.IN_PROGRESS.name()); // when @@ -68,7 +70,7 @@ void create_pair_room() { void create_timer_when_create_pair_room() { // given final PairRoomCreateRequest request = - new PairRoomCreateRequest("레디", "프람", 1000L, 100L, + new PairRoomCreateRequest("레디", "프람", 1000L, 100L, "https://missionUrl.xxx", PairRoomStatus.IN_PROGRESS.name()); // when @@ -80,7 +82,6 @@ void create_timer_when_create_pair_room() { @Test - @Transactional @DisplayName("존재하지 않는 페어룸 접근 코드를 찾으면 예외가 발생한다.") void throw_exception_when_find_not_exist_access_code() { // given @@ -91,12 +92,27 @@ void throw_exception_when_find_not_exist_access_code() { .isExactlyInstanceOf(PairRoomNotFoundException.class); } + @Test + @DisplayName("삭제된 페어룸의 접근 코드를 찾으면 예외가 발생한다.") + void throw_exception_when_find_delete_pair_room_access_code() { + // given + final PairRoomCreateRequest request = + new PairRoomCreateRequest("레디", "프람", 1000L, 100L, + "https://missionUrl.xxx", PairRoomStatus.DELETED.name()); + final String accessCode = pairRoomService.savePairRoom(request, null); + + // when & then + assertThatThrownBy(() -> pairRoomService.findPairRoomAndTimer(accessCode)) + .isExactlyInstanceOf(DeletePairRoomException.class); + } + @Test @DisplayName("페어룸 상태를 변경한다.") void update_pair_room_status() { // given final PairRoomCreateRequest request = - new PairRoomCreateRequest("레디", "프람", 1000L, 100L, PairRoomStatus.IN_PROGRESS.name()); + new PairRoomCreateRequest("레디", "프람", 1000L, 100L, "https://missionUrl.xxx", + PairRoomStatus.IN_PROGRESS.name()); final String accessCode = pairRoomService.savePairRoom(request, null); // when @@ -107,6 +123,20 @@ void update_pair_room_status() { .isEqualTo(PairRoomStatus.COMPLETED); } + @Test + @DisplayName("삭제된 페어룸 상태를 변경하려고 하면 예외를 발생시킨다.") + void update_delete_pair_room_status() { + // given + final PairRoomCreateRequest request = + new PairRoomCreateRequest("레디", "프람", 1000L, 100L, "https://missionUrl.xxx", + PairRoomStatus.DELETED.name()); + final String accessCode = pairRoomService.savePairRoom(request, null); + + // when & then + assertThatThrownBy(() -> pairRoomService.updatePairRoomStatus(accessCode, PairRoomStatus.COMPLETED.name())) + .isExactlyInstanceOf(DeletePairRoomException.class); + } + @Test @DisplayName("페어 역할을 변경한다.") void change_pair_room() { @@ -114,6 +144,7 @@ void change_pair_room() { final PairRoomEntity entity = PairRoomEntity.from( new PairRoom(PairRoomStatus.IN_PROGRESS, new Pair(new PairName("fram"), new PairName("lemonL")), + new MissionUrl("https://missionUrl.xxx"), new AccessCode("1234")) ); pairRoomRepository.save(entity); @@ -127,8 +158,25 @@ void change_pair_room() { .contains("lemonL", "fram"); } + @Test + @DisplayName("삭제된 페어룸의 페어 역할을 변경하려하면 예외를 발생시킨다.") + void change_delete_pair_room_role() { + // given + final PairRoomEntity entity = PairRoomEntity.from( + new PairRoom(PairRoomStatus.DELETED, + new Pair(new PairName("fram"), new PairName("lemonL")), + new MissionUrl("https://missionUrl.xxx"), + new AccessCode("1234")) + ); + pairRoomRepository.save(entity); + + // when & then + assertThatThrownBy(() -> pairRoomService.updateNavigatorWithDriver(entity.getAccessCode())) + .isExactlyInstanceOf(DeletePairRoomException.class); + } + - @DisplayName("멤버의 방 목록을 가져온다.") + @DisplayName("삭제되지 않은, 멤버의 방 목록을 가져온다.") @Test void find_rooms_by_member() { //given @@ -136,6 +184,7 @@ void find_rooms_by_member() { final Member memberB = createMember("test"); final PairRoomCreateRequest pairRoomCreateRequest = new PairRoomCreateRequest("레디", "잉크", 1, 1, + "https://missionUrl.xxx", "IN_PROGRESS"); final String accessCodeA_1 = pairRoomService.savePairRoom(pairRoomCreateRequest, memberA.getAccessToken()); @@ -143,6 +192,12 @@ void find_rooms_by_member() { final String accessCodeB_1 = pairRoomService.savePairRoom(pairRoomCreateRequest, memberB.getAccessToken()); pairRoomService.savePairRoom(pairRoomCreateRequest, null); + final PairRoomCreateRequest deletePairRoomCreateRequest = new PairRoomCreateRequest("레디", "잉크", 1, 1, + "https://missionUrl.xxx", PairRoomStatus.DELETED.name()); + pairRoomService.savePairRoom(deletePairRoomCreateRequest, memberA.getAccessToken()); + pairRoomService.savePairRoom(deletePairRoomCreateRequest, memberA.getAccessToken()); + pairRoomService.savePairRoom(deletePairRoomCreateRequest, memberA.getAccessToken()); + final List memberAExpected = List.of(accessCodeA_1, accessCodeA_2); final List memberBExpected = List.of(accessCodeB_1); @@ -182,6 +237,7 @@ void get_pair_room_and_timer() { final PairRoomEntity pairRoomEntity = PairRoomEntity.from( new PairRoom(PairRoomStatus.IN_PROGRESS, new Pair(new PairName("레디"), new PairName("파슬리")), + new MissionUrl("https://missionUrl.xxx"), new AccessCode("123456")) ); final Timer timer = new Timer( @@ -211,6 +267,7 @@ void exists_pair_room() { final PairRoomEntity pairRoomEntity = PairRoomEntity.from( new PairRoom(PairRoomStatus.IN_PROGRESS, new Pair(new PairName("레디"), new PairName("레모네")), + new MissionUrl("https://missionUrl.xxx"), accessCode )); pairRoomRepository.save(pairRoomEntity); diff --git a/backend/src/test/java/site/coduo/timer/domain/TimerTest.java b/backend/src/test/java/site/coduo/timer/domain/TimerTest.java index 8529cecd..acd5e763 100644 --- a/backend/src/test/java/site/coduo/timer/domain/TimerTest.java +++ b/backend/src/test/java/site/coduo/timer/domain/TimerTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import site.coduo.pairroom.domain.MissionUrl; import site.coduo.pairroom.domain.Pair; import site.coduo.pairroom.domain.PairName; import site.coduo.pairroom.domain.PairRoom; @@ -74,6 +75,7 @@ private PairRoom createPairRoom(final String navigator, final String driver) { return new PairRoom( PairRoomStatus.IN_PROGRESS, new Pair(new PairName(navigator), new PairName(driver)), + new MissionUrl("https://missionUrl.xxx"), new AccessCode("123456") ); } diff --git a/backend/src/test/java/site/coduo/timer/repository/TimerRepositoryTest.java b/backend/src/test/java/site/coduo/timer/repository/TimerRepositoryTest.java index 29acaa9b..f527d8e0 100644 --- a/backend/src/test/java/site/coduo/timer/repository/TimerRepositoryTest.java +++ b/backend/src/test/java/site/coduo/timer/repository/TimerRepositoryTest.java @@ -9,6 +9,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; +import site.coduo.pairroom.domain.MissionUrl; import site.coduo.pairroom.domain.Pair; import site.coduo.pairroom.domain.PairName; import site.coduo.pairroom.domain.PairRoom; @@ -41,6 +42,7 @@ void inquiry_timer() { final PairRoom pairRoom = new PairRoom( PairRoomStatus.IN_PROGRESS, new Pair(new PairName("레머네"), new PairName("프람")), + new MissionUrl("https://missionUrl.xxx"), new AccessCode("hello1") ); final PairRoomEntity entity = site.coduo.pairroom.repository.PairRoomEntity.from( diff --git a/backend/src/test/java/site/coduo/timer/service/TimerServiceTest.java b/backend/src/test/java/site/coduo/timer/service/TimerServiceTest.java index 98b17062..cdad8f90 100644 --- a/backend/src/test/java/site/coduo/timer/service/TimerServiceTest.java +++ b/backend/src/test/java/site/coduo/timer/service/TimerServiceTest.java @@ -42,8 +42,8 @@ void tearDown() { @DisplayName("타이머를 저장한다.") void create_timer() { // given - final PairRoomCreateRequest request = new PairRoomCreateRequest("켈리", "레모네", 10000L, 1000L, - PairRoomStatus.IN_PROGRESS.name()); + final PairRoomCreateRequest request = new PairRoomCreateRequest("켈리", "레모네", 10000L, + 1000L, "https://missionUrl.xxx", PairRoomStatus.IN_PROGRESS.name()); // when & then assertThatCode(() -> pairRoomService.savePairRoom(request, null)) @@ -54,8 +54,8 @@ void create_timer() { @DisplayName("타이머를 반환한다.") void get_latest_timer() { // given - final PairRoomCreateRequest request = new PairRoomCreateRequest("잉크", "레디", 1000L, 1000L, - PairRoomStatus.IN_PROGRESS.name()); + final PairRoomCreateRequest request = new PairRoomCreateRequest("잉크", "레디", 1000L, + 1000L, "https://missionUrl.xxx", PairRoomStatus.IN_PROGRESS.name()); final String accessCode = pairRoomService.savePairRoom(request, null); // when @@ -72,8 +72,8 @@ void get_latest_timer() { @DisplayName("타이머를 업데이트 한다.") void update_timer() { // given - final PairRoomCreateRequest request = new PairRoomCreateRequest("잉크", "레디", 10000000L, 100L, - PairRoomStatus.IN_PROGRESS.name()); + final PairRoomCreateRequest request = new PairRoomCreateRequest("잉크", "레디", 10000000L, + 100L, "https://missionUrl.xxx", PairRoomStatus.IN_PROGRESS.name()); final String accessCode = pairRoomService.savePairRoom(request, null); final TimerUpdateRequest timerRequest = new TimerUpdateRequest(10000, 5000); @@ -92,8 +92,8 @@ void update_timer() { @DisplayName("타이머 남은 시간을 반환한다. - 타이머 타임 스탬프가 존재할 경우") void get_remaining_time_when_exist_timestamp() { // given - final PairRoomCreateRequest pairRoomCreateRequest = new PairRoomCreateRequest("켈리", "레모네", 3000L, 3000L, - PairRoomStatus.IN_PROGRESS.name()); + final PairRoomCreateRequest pairRoomCreateRequest = new PairRoomCreateRequest("켈리", "레모네", + 3000L, 3000L, "https://missionUrl.xxx", PairRoomStatus.IN_PROGRESS.name()); final String accessCode = pairRoomService.savePairRoom(pairRoomCreateRequest, null); final Timer timeStamp = new Timer(new AccessCode(accessCode), 10000L, 10000L); timestampRegistry.register(accessCode, timeStamp); @@ -109,8 +109,8 @@ void get_remaining_time_when_exist_timestamp() { @DisplayName("타이머 남은 시간을 반환한다. - 타이머가 한번도 동작하지 않았을 경우") void get_remaining_time_when_not_exist_timestamp() { // given - final PairRoomCreateRequest pairRoomCreateRequest = new PairRoomCreateRequest("켈리", "레모네", 3000L, 3000L, - PairRoomStatus.IN_PROGRESS.name()); + final PairRoomCreateRequest pairRoomCreateRequest = new PairRoomCreateRequest("켈리", "레모네", + 3000L, 3000L, "https://missionUrl.xxx", PairRoomStatus.IN_PROGRESS.name()); final String accessCode = pairRoomService.savePairRoom(pairRoomCreateRequest, null); // when diff --git a/backend/src/test/java/site/coduo/todo/service/TodoServiceTest.java b/backend/src/test/java/site/coduo/todo/service/TodoServiceTest.java index 9e165e72..a09b6a47 100644 --- a/backend/src/test/java/site/coduo/todo/service/TodoServiceTest.java +++ b/backend/src/test/java/site/coduo/todo/service/TodoServiceTest.java @@ -45,6 +45,19 @@ class TodoServiceTest { @Autowired private TodoService todoService; + private static Stream destinationSortAndExpectOrder() { + return Stream.of( + Arguments.of(0, List.of("content4!", "content1!", "content2!", "content3!", "content5!", "content6!", + "content7!")), + Arguments.of(6, List.of("content1!", "content2!", "content3!", "content5!", "content6!", "content7!", + "content4!")), + Arguments.of(1, List.of("content1!", "content4!", "content2!", "content3!", "content5!", "content6!", + "content7!")), + Arguments.of(5, List.of("content1!", "content2!", "content3!", "content5!", "content6!", "content4!", + "content7!")) + ); + } + @AfterEach void clean() { todoRepository.deleteAll(); @@ -56,7 +69,7 @@ void clean() { void createTodo() { // Given final String accessCode = pairRoomService.savePairRoom( - new PairRoomCreateRequest("A", "B", 60_000, 60_000, "IN_PROGRESS"), null); + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); final String content = "content!"; // When @@ -77,7 +90,8 @@ void createTodo() { @Test void createPairRoomWithNotFoundPairRoomId() { // Given - pairRoomService.savePairRoom(new PairRoomCreateRequest("A", "B", 60_000, 60_000, "IN_PROGRESS"), null); + pairRoomService.savePairRoom( + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); final String content = "content!"; // When & Then @@ -91,7 +105,7 @@ void createPairRoomWithNotFoundPairRoomId() { void updateTodoContent() { // Given final String accessCode = pairRoomService.savePairRoom( - new PairRoomCreateRequest("A", "B", 60_000, 60_000, "IN_PROGRESS"), null); + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); final String content = "content!"; @@ -119,7 +133,7 @@ void updateTodoContent() { void updateTodoContentWithNotFoundTodoId() { // Given final String accessCode = pairRoomService.savePairRoom( - new PairRoomCreateRequest("A", "B", 60_000, 60_000, "IN_PROGRESS"), null); + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); final String content = "content!"; @@ -143,7 +157,7 @@ void updateTodoContentWithNotFoundTodoId() { void toggleTodoChecked() { // Given final String accessCode = pairRoomService.savePairRoom( - new PairRoomCreateRequest("A", "B", 60_000, 60_000, "IN_PROGRESS"), null); + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); final String content = "content!"; @@ -169,7 +183,7 @@ void toggleTodoChecked() { void toggleTodoCheckedWithNotFoundTodoId() { // Given final String accessCode = pairRoomService.savePairRoom( - new PairRoomCreateRequest("A", "B", 60_000, 60_000, "IN_PROGRESS"), null); + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); final String content = "content!"; @@ -192,7 +206,7 @@ void toggleTodoCheckedWithNotFoundTodoId() { void deleteTodo() { // Given final String accessCode = pairRoomService.savePairRoom( - new PairRoomCreateRequest("A", "B", 60_000, 60_000, "IN_PROGRESS"), null); + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); final String content = "content!"; @@ -219,7 +233,7 @@ void deleteTodo() { void getAll() { // Given final String accessCode = pairRoomService.savePairRoom( - new PairRoomCreateRequest("A", "B", 60_000, 60_000, "IN_PROGRESS"), null); + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); final List todos = List.of( @@ -256,7 +270,7 @@ void getAll() { void getAllOrderBySortWithNotExistPairRoomId() { // Given final String accessCode = pairRoomService.savePairRoom( - new PairRoomCreateRequest("A", "B", 60_000, 60_000, "IN_PROGRESS"), null); + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); final List todos = List.of( @@ -284,7 +298,7 @@ void getAllOrderBySortWithNotExistPairRoomId() { void updateTodoSort(final int destinationSort, final List expect) { // Given final String accessCode = pairRoomService.savePairRoom( - new PairRoomCreateRequest("A", "B", 60_000, 60_000, "IN_PROGRESS"), null); + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); final List todos = List.of( @@ -316,25 +330,12 @@ void updateTodoSort(final int destinationSort, final List expect) { assertThat(orders).isEqualTo(expect); } - private static Stream destinationSortAndExpectOrder() { - return Stream.of( - Arguments.of(0, List.of("content4!", "content1!", "content2!", "content3!", "content5!", "content6!", - "content7!")), - Arguments.of(6, List.of("content1!", "content2!", "content3!", "content5!", "content6!", "content7!", - "content4!")), - Arguments.of(1, List.of("content1!", "content4!", "content2!", "content3!", "content5!", "content6!", - "content7!")), - Arguments.of(5, List.of("content1!", "content2!", "content3!", "content5!", "content6!", "content4!", - "content7!")) - ); - } - @DisplayName("존재하지 않은 페어룸 아이디와 함께 투두 순서 변경 요청을 하면 예외를 발생시킨다.") @Test void updateTodoSortWithNotExistPairRoomId() { // Given final String accessCode = pairRoomService.savePairRoom( - new PairRoomCreateRequest("A", "B", 60_000, 60_000, "IN_PROGRESS"), null); + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); final List todos = List.of(