diff --git a/src/main/java/com/recyclestudy/member/controller/DeviceController.java b/src/main/java/com/recyclestudy/member/controller/DeviceController.java index 6ac6434..dcc0b97 100644 --- a/src/main/java/com/recyclestudy/member/controller/DeviceController.java +++ b/src/main/java/com/recyclestudy/member/controller/DeviceController.java @@ -38,8 +38,7 @@ public ResponseEntity deleteDevice( @AuthDevice final DeviceIdentifier identifier, @RequestBody final DeviceDeleteRequest request ) { - final DeviceDeleteInput input = DeviceDeleteInput.from(request.email(), identifier, - request.targetDeviceIdentifier()); + final DeviceDeleteInput input = DeviceDeleteInput.from(identifier, request.targetDeviceIdentifier()); memberService.deleteDevice(input); return ResponseEntity.noContent().build(); } diff --git a/src/main/java/com/recyclestudy/member/controller/MemberController.java b/src/main/java/com/recyclestudy/member/controller/MemberController.java index 10b81f6..56f5dbe 100644 --- a/src/main/java/com/recyclestudy/member/controller/MemberController.java +++ b/src/main/java/com/recyclestudy/member/controller/MemberController.java @@ -23,7 +23,6 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -46,12 +45,8 @@ public ResponseEntity saveMember(@RequestBody final MemberSa } @GetMapping - public ResponseEntity findAllMemberDevices( - @RequestParam(name = "email") final String email, - @AuthDevice final DeviceIdentifier identifier - ) { - - final MemberFindInput input = MemberFindInput.from(email, identifier); + public ResponseEntity findAllMemberDevices(@AuthDevice final DeviceIdentifier identifier) { + final MemberFindInput input = MemberFindInput.from(identifier); final MemberFindOutput output = memberService.findAllMemberDevices(input); final MemberFindResponse response = MemberFindResponse.from(output); return ResponseEntity.ok(response); diff --git a/src/main/java/com/recyclestudy/member/service/MemberService.java b/src/main/java/com/recyclestudy/member/service/MemberService.java index a61f2e9..4d1bc18 100644 --- a/src/main/java/com/recyclestudy/member/service/MemberService.java +++ b/src/main/java/com/recyclestudy/member/service/MemberService.java @@ -53,8 +53,11 @@ public MemberSaveOutput saveDevice(final MemberSaveInput input) { @Transactional(readOnly = true) public MemberFindOutput findAllMemberDevices(final MemberFindInput input) { - final List devices = deviceRepository.findAllByMemberEmail(input.email()); - return MemberFindOutput.of(input.email(), devices); + final Member member = memberRepository.findByIdentifier(input.deviceIdentifier()) + .orElseThrow(() -> new UnauthorizedException("인증되지 않은 디바이스입니다")); + + final List devices = deviceRepository.findAllByMemberEmail(member.getEmail()); + return MemberFindOutput.of(member.getEmail(), devices); } @Transactional(readOnly = true) @@ -69,11 +72,11 @@ public void authenticateDevice(final Email email, final DeviceIdentifier deviceI checkExistedMember(email); final Device device = deviceRepository.findByIdentifier(deviceIdentifier) - .orElseThrow(() -> new NotFoundException("존재하지 않는 디바이스 아이디입니다: %s" + .orElseThrow(() -> new NotFoundException("존재하지 않는 디바이스 식별자입니다: %s" .formatted(deviceIdentifier.getValue()))); if (device.isActive()) { - throw new BadRequestException("이미 인증되었습니다"); + throw new BadRequestException("이미 인증된 디바이스입니다"); } device.verifyOwner(email); @@ -83,7 +86,18 @@ public void authenticateDevice(final Email email, final DeviceIdentifier deviceI @Transactional public void deleteDevice(final DeviceDeleteInput input) { - deviceRepository.deleteByIdentifier(input.targetDeviceIdentifier()); + final Member requestMember = memberRepository.findByIdentifier(input.deviceIdentifier()) + .orElseThrow(() -> new UnauthorizedException("유효하지 않은 디바이스입니다")); + + final Device targetDevice = deviceRepository.findByIdentifier(input.targetDeviceIdentifier()) + .orElseThrow(() -> new NotFoundException("존재하지 않는 디바이스입니다: %s" + .formatted(input.targetDeviceIdentifier().getValue()))); + + if (!targetDevice.getMember().hasEmail(requestMember.getEmail())) { + throw new NotFoundException("존재하지 않는 디바이스입니다: %s".formatted(input.targetDeviceIdentifier().getValue())); + } + + deviceRepository.delete(targetDevice); log.info("[DEVICE_DELETED] 디바이스 삭제 성공: {}", input.targetDeviceIdentifier()); } diff --git a/src/main/java/com/recyclestudy/member/service/input/DeviceDeleteInput.java b/src/main/java/com/recyclestudy/member/service/input/DeviceDeleteInput.java index d0dcd95..f092607 100644 --- a/src/main/java/com/recyclestudy/member/service/input/DeviceDeleteInput.java +++ b/src/main/java/com/recyclestudy/member/service/input/DeviceDeleteInput.java @@ -1,18 +1,14 @@ package com.recyclestudy.member.service.input; import com.recyclestudy.member.domain.DeviceIdentifier; -import com.recyclestudy.member.domain.Email; -public record DeviceDeleteInput(Email email, DeviceIdentifier deviceIdentifier, - DeviceIdentifier targetDeviceIdentifier) { +public record DeviceDeleteInput(DeviceIdentifier deviceIdentifier, DeviceIdentifier targetDeviceIdentifier) { public static DeviceDeleteInput from( - final String emailValue, final DeviceIdentifier identifier, final String targetIdentifier ) { - final Email email = Email.from(emailValue); final DeviceIdentifier targetDeviceIdentifier = DeviceIdentifier.from(targetIdentifier); - return new DeviceDeleteInput(email, identifier, targetDeviceIdentifier); + return new DeviceDeleteInput(identifier, targetDeviceIdentifier); } } diff --git a/src/main/java/com/recyclestudy/member/service/input/MemberFindInput.java b/src/main/java/com/recyclestudy/member/service/input/MemberFindInput.java index 9c714f1..ef6a2f7 100644 --- a/src/main/java/com/recyclestudy/member/service/input/MemberFindInput.java +++ b/src/main/java/com/recyclestudy/member/service/input/MemberFindInput.java @@ -1,12 +1,10 @@ package com.recyclestudy.member.service.input; import com.recyclestudy.member.domain.DeviceIdentifier; -import com.recyclestudy.member.domain.Email; -public record MemberFindInput(Email email, DeviceIdentifier deviceIdentifier) { +public record MemberFindInput(DeviceIdentifier deviceIdentifier) { - public static MemberFindInput from(final String emailValue, final DeviceIdentifier identifier) { - final Email email = Email.from(emailValue); - return new MemberFindInput(email, identifier); + public static MemberFindInput from(final DeviceIdentifier identifier) { + return new MemberFindInput(identifier); } } diff --git a/src/test/java/com/recyclestudy/member/controller/DeviceControllerTest.java b/src/test/java/com/recyclestudy/member/controller/DeviceControllerTest.java index 412371d..bf8d633 100644 --- a/src/test/java/com/recyclestudy/member/controller/DeviceControllerTest.java +++ b/src/test/java/com/recyclestudy/member/controller/DeviceControllerTest.java @@ -59,7 +59,7 @@ void setUpMocks(RestDocumentationContextProvider provider) { } @Test - @DisplayName("디바이스 인증 성공 시 200 응답을 반환한다") + @DisplayName("디바이스 인증에 성공하면 200 OK와 함께 인증 성공 뷰를 반환한다") void authenticateDevice_Success() { // given final String email = "test@test.com"; @@ -75,7 +75,7 @@ void authenticateDevice_Success() { builder() .tag("Device") .summary("디바이스 인증") - .description("디바이스 인증 성공 시 인증 완료 안내 HTML 페이지를 반환합니다.") + .description("디바이스 인증에 성공하면 인증 성공 뷰(auth_success.html)를 반환한다") .queryParameters( parameterWithName("email").description("이메일"), parameterWithName("identifier").description("디바이스 식별자") @@ -103,13 +103,13 @@ void authenticateDevice_Success() { } @Test - @DisplayName("이미 인증된 디바이스 인증 시도 시 400 응답을 반환한다") + @DisplayName("이미 인증된 디바이스를 인증하려고 하면 400 Bad Request를 반환한다") void authenticateDevice_AlreadyAuthenticated() { // given final String email = "test@test.com"; final String identifier = "device-identifier"; - doThrow(new BadRequestException("이미 인증되었습니다")) + doThrow(new BadRequestException("이미 인증된 디바이스입니다")) .when(memberService).authenticateDevice(any(Email.class), any(DeviceIdentifier.class)); // when @@ -119,7 +119,7 @@ void authenticateDevice_AlreadyAuthenticated() { builder() .tag("Device") .summary("디바이스 인증") - .description("이미 인증된 디바이스 인증 시도 시 400 응답을 반환한다") + .description("이미 인증된 디바이스를 인증하려고 하면 400 Bad Request를 반환한다") .queryParameters( parameterWithName("email").description("이메일"), parameterWithName("identifier").description("디바이스 식별자") @@ -134,11 +134,11 @@ void authenticateDevice_AlreadyAuthenticated() { .get("/api/v1/device/auth") .then() .statusCode(HttpStatus.BAD_REQUEST.value()) - .body("message", equalTo("이미 인증되었습니다")); + .body("message", equalTo("이미 인증된 디바이스입니다")); } @Test - @DisplayName("인증 시 유효하지 않은 이메일 형식인 경우 400 응답을 반환한다") + @DisplayName("인증 요청 시 이메일 형식이 올바르지 않으면 400 Bad Request를 반환한다") void authenticateDevice_InvalidEmailFormat() { // given final String invalidEmail = "invalid-email"; @@ -151,7 +151,7 @@ void authenticateDevice_InvalidEmailFormat() { builder() .tag("Device") .summary("디바이스 인증") - .description("유효하지 않은 이메일 형식인 경우 400 응답을 반환한다") + .description("인증 요청 시 이메일 형식이 올바르지 않으면 400 Bad Request를 반환한다") .queryParameters( parameterWithName("email").description("이메일"), parameterWithName("identifier").description("디바이스 식별자") @@ -170,13 +170,13 @@ void authenticateDevice_InvalidEmailFormat() { } @Test - @DisplayName("인증 유효 시간이 만료된 경우 400 응답을 반환한다") + @DisplayName("인증 유효 시간이 만료된 경우 400 Bad Request를 반환한다") void authenticateDevice_Expired() { // given final String email = "test@test.com"; final String identifier = "device-identifier"; - doThrow(new DeviceActivationExpiredException("인증 유효 시간이 만료되었습니다.")) + doThrow(new DeviceActivationExpiredException("인증 유효 시간이 만료되었습니다")) .when(memberService).authenticateDevice(any(Email.class), any(DeviceIdentifier.class)); // when @@ -186,7 +186,7 @@ void authenticateDevice_Expired() { builder() .tag("Device") .summary("디바이스 인증") - .description("인증 유효 시간이 만료된 경우 400 응답을 반환한다") + .description("인증 유효 시간이 만료된 경우 400 Bad Request를 반환한다") .queryParameters( parameterWithName("email").description("이메일"), parameterWithName("identifier").description("디바이스 식별자") @@ -201,11 +201,11 @@ void authenticateDevice_Expired() { .get("/api/v1/device/auth") .then() .statusCode(HttpStatus.BAD_REQUEST.value()) - .body("message", equalTo("인증 유효 시간이 만료되었습니다.")); + .body("message", equalTo("인증 유효 시간이 만료되었습니다")); } @Test - @DisplayName("디바이스 삭제 시 204 응답을 반환한다") + @DisplayName("디바이스를 삭제하면 204 No Content를 반환한다") void deleteDevice() { // given final String headerIdentifier = "device-id"; @@ -220,12 +220,13 @@ void deleteDevice() { builder() .tag("Device") .summary("디바이스 삭제") - .description("디바이스 삭제 시 204 응답을 반환한다") + .description("디바이스를 삭제하면 204 No Content를 반환한다") .requestHeaders( headerWithName("X-Device-Id").description("디바이스 식별자") ) .requestFields( - fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"), + fieldWithPath("email").type(JsonFieldType.STRING) + .description("이메일 (서버 인증 로직에서 사용되지 않음)").optional(), fieldWithPath("targetDeviceIdentifier").type(JsonFieldType.STRING) .description("삭제할 디바이스 식별자") ) @@ -240,7 +241,7 @@ void deleteDevice() { } @Test - @DisplayName("존재하지 않는 멤버의 이메일로 인증 시도 시 404 응답을 반환한다") + @DisplayName("존재하지 않는 멤버의 디바이스를 인증하려고 하면 404 Not Found를 반환한다") void authenticateDevice_NotFoundMember() { // given final String email = "notfound@test.com"; @@ -256,7 +257,7 @@ void authenticateDevice_NotFoundMember() { builder() .tag("Device") .summary("디바이스 인증") - .description("존재하지 않는 멤버의 이메일로 인증 시도 시 404 응답을 반환한다") + .description("존재하지 않는 멤버의 디바이스를 인증하려고 하면 404 Not Found를 반환한다") .queryParameters( parameterWithName("email").description("이메일"), parameterWithName("identifier").description("디바이스 식별자") @@ -275,13 +276,13 @@ void authenticateDevice_NotFoundMember() { } @Test - @DisplayName("존재하지 않는 디바이스 식별자로 인증 시도 시 404 응답을 반환한다") + @DisplayName("존재하지 않는 디바이스 식별자로 인증하려고 하면 404 Not Found를 반환한다") void authenticateDevice_NotFoundDevice() { // given final String email = "test@test.com"; final String identifier = "not-found-id"; - doThrow(new NotFoundException("존재하지 않는 디바이스 아이디입니다")) + doThrow(new NotFoundException("존재하지 않는 디바이스 식별자입니다")) .when(memberService).authenticateDevice(any(Email.class), any(DeviceIdentifier.class)); // when @@ -291,7 +292,7 @@ void authenticateDevice_NotFoundDevice() { builder() .tag("Device") .summary("디바이스 인증") - .description("존재하지 않는 디바이스 식별자로 인증 시도 시 404 응답을 반환한다") + .description("존재하지 않는 디바이스 식별자로 인증하려고 하면 404 Not Found를 반환한다") .queryParameters( parameterWithName("email").description("이메일"), parameterWithName("identifier").description("디바이스 식별자") @@ -306,11 +307,11 @@ void authenticateDevice_NotFoundDevice() { .get("/api/v1/device/auth") .then() .statusCode(HttpStatus.NOT_FOUND.value()) - .body("message", equalTo("존재하지 않는 디바이스 아이디입니다")); + .body("message", equalTo("존재하지 않는 디바이스 식별자입니다")); } @Test - @DisplayName("디바이스 소유자가 아닌 이메일로 인증 시도 시 400 응답을 반환한다") + @DisplayName("디바이스 소유자가 아닌 회원이 인증하려고 하면 400 Bad Request를 반환한다") void authenticateDevice_NotOwner() { // given final String email = "other@test.com"; @@ -326,7 +327,7 @@ void authenticateDevice_NotOwner() { builder() .tag("Device") .summary("디바이스 인증") - .description("디바이스 소유자가 아닌 이메일로 인증 시도 시 400 응답을 반환한다") + .description("디바이스 소유자가 아닌 회원이 인증하려고 하면 400 Bad Request를 반환한다") .queryParameters( parameterWithName("email").description("이메일"), parameterWithName("identifier").description("디바이스 식별자") @@ -345,7 +346,7 @@ void authenticateDevice_NotOwner() { } @Test - @DisplayName("인증 시 이메일 파라미터가 누락된 경우 400 응답을 반환한다") + @DisplayName("인증 요청 시 이메일이 누락되면 400 Bad Request를 반환한다") void authenticateDevice_NullEmail() { // given final String identifier = "device-identifier"; @@ -357,7 +358,7 @@ void authenticateDevice_NullEmail() { builder() .tag("Device") .summary("디바이스 인증") - .description("이메일 파라미터가 누락된 경우 400 응답을 반환한다") + .description("인증 요청 시 이메일이 누락되면 400 Bad Request를 반환한다") .queryParameters( parameterWithName("identifier").description("디바이스 식별자") ) @@ -373,7 +374,7 @@ void authenticateDevice_NullEmail() { } @Test - @DisplayName("인증 시 디바이스 식별자 파라미터가 누락된 경우 400 응답을 반환한다") + @DisplayName("인증 요청 시 디바이스 식별자가 누락되면 400 Bad Request를 반환한다") void authenticateDevice_NullIdentifier() { // given final String email = "test@test.com"; @@ -385,7 +386,7 @@ void authenticateDevice_NullIdentifier() { builder() .tag("Device") .summary("디바이스 인증") - .description("디바이스 식별자 파라미터가 누락된 경우 400 응답을 반환한다") + .description("인증 요청 시 디바이스 식별자가 누락되면 400 Bad Request를 반환한다") .queryParameters( parameterWithName("email").description("이메일"), parameterWithName("identifier").description("디바이스 식별자") @@ -402,11 +403,12 @@ void authenticateDevice_NullIdentifier() { } @Test - @DisplayName("삭제 요청 시 이메일이 누락된 경우 400 응답을 반환한다") + @DisplayName("디바이스 삭제 요청 시 이메일이 누락되어도 정상 처리된다 (Legacy compatibility)") void deleteDevice_NullEmail() { // given final String headerIdentifier = "device-id"; final DeviceDeleteRequest request = new DeviceDeleteRequest(null, "target-id"); + doNothing().when(memberService).deleteDevice(any()); // when // then @@ -415,18 +417,16 @@ void deleteDevice_NullEmail() { builder() .tag("Device") .summary("디바이스 삭제") - .description("삭제 요청 시 이메일이 누락된 경우 400 응답을 반환한다") + .description("디바이스 삭제 요청 시 이메일이 누락되어도 서버 인증 로직에서 사용되지 않으므로 정상 처리된다") .requestHeaders( headerWithName("X-Device-Id").description("디바이스 식별자") ) .requestFields( - fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"), + fieldWithPath("email").type(JsonFieldType.STRING) + .description("이메일 (서버 인증 로직에서 사용되지 않음)").optional(), fieldWithPath("targetDeviceIdentifier").type(JsonFieldType.STRING) .description("삭제할 디바이스 식별자") ) - .responseFields( - fieldWithPath("message").type(JsonFieldType.STRING).description("에러 메시지") - ) )) .contentType(MediaType.APPLICATION_JSON_VALUE) .header("X-Device-Id", headerIdentifier) @@ -434,12 +434,11 @@ void deleteDevice_NullEmail() { .when() .delete("/api/v1/device") .then() - .statusCode(HttpStatus.BAD_REQUEST.value()) - .body("message", equalTo("null이 될 수 없습니다: value")); + .statusCode(HttpStatus.NO_CONTENT.value()); } @Test - @DisplayName("삭제 요청 시 삭제할 디바이스 식별자가 누락된 경우 400 응답을 반환한다") + @DisplayName("디바이스 삭제 요청 시 대상 디바이스 식별자가 누락되면 400 Bad Request를 반환한다") void deleteDevice_NullTargetIdentifier() { // given final String headerIdentifier = "device-id"; @@ -452,12 +451,13 @@ void deleteDevice_NullTargetIdentifier() { builder() .tag("Device") .summary("디바이스 삭제") - .description("삭제 요청 시 삭제할 디바이스 식별자가 누락된 경우 400 응답을 반환한다") + .description("디바이스 삭제 요청 시 대상 디바이스 식별자가 누락되면 400 Bad Request를 반환한다") .requestHeaders( headerWithName("X-Device-Id").description("디바이스 식별자") ) .requestFields( - fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"), + fieldWithPath("email").type(JsonFieldType.STRING) + .description("이메일 (서버 인증 로직에서 사용되지 않음)").optional(), fieldWithPath("targetDeviceIdentifier").type(JsonFieldType.STRING) .description("삭제할 디바이스 식별자") ) @@ -476,7 +476,7 @@ void deleteDevice_NullTargetIdentifier() { } @Test - @DisplayName("헤더로 디바이스 인증하여 삭제 시 204 응답을 반환한다") + @DisplayName("헤더로 디바이스 인증 후 삭제하면 204 No Content를 반환한다") void deleteDevice_WithHeader() { // given final String headerIdentifier = "device-id"; @@ -491,13 +491,14 @@ void deleteDevice_WithHeader() { builder() .tag("Device") .summary("디바이스 삭제") - .description("헤더로 디바이스 인증하여 삭제 시 204 응답을 반환한다") + .description("헤더로 디바이스 인증 후 삭제하면 204 No Content를 반환한다") .requestHeaders( headerWithName("X-Device-Id").description("디바이스 식별자") ) .requestFields( - fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"), - fieldWithPath("targetIdentifier").type(JsonFieldType.STRING) + fieldWithPath("email").type(JsonFieldType.STRING) + .description("이메일 (서버 인증 로직에서 사용되지 않음)").optional(), + fieldWithPath("targetDeviceIdentifier").type(JsonFieldType.STRING) .description("삭제할 디바이스 식별자") ) )) diff --git a/src/test/java/com/recyclestudy/member/controller/MemberControllerTest.java b/src/test/java/com/recyclestudy/member/controller/MemberControllerTest.java index 53c7901..651a3c7 100644 --- a/src/test/java/com/recyclestudy/member/controller/MemberControllerTest.java +++ b/src/test/java/com/recyclestudy/member/controller/MemberControllerTest.java @@ -139,9 +139,6 @@ void findAllMemberDevices() { .requestHeaders( headerWithName("X-Device-Id").description("디바이스 식별자") ) - .queryParameters( - parameterWithName("email").description("이메일") - ) .responseFields( fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"), fieldWithPath("devices").type(JsonFieldType.ARRAY).description("디바이스 목록"), @@ -149,13 +146,9 @@ void findAllMemberDevices() { .description("디바이스 식별자 값"), fieldWithPath("devices[].createdAt").type(JsonFieldType.STRING) .description("디바이스 생성일") - ), - queryParameters( - parameterWithName("email").description("이메일") - ) + ) )) .header("X-Device-Id", headerIdentifier) - .param("email", email) .when() .get("/api/v1/members") .then() @@ -185,13 +178,13 @@ void findAllMemberDevices_NotFoundMember() { headerWithName("X-Device-Id").description("디바이스 식별자") ) .queryParameters( - parameterWithName("email").description("이메일") + parameterWithName("email").description("이메일 (다음 버전에서 제거 예정)") ) .responseFields( fieldWithPath("message").type(JsonFieldType.STRING).description("에러 메시지") ), queryParameters( - parameterWithName("email").description("이메일") + parameterWithName("email").description("이메일 (다음 버전에서 제거 예정)") ) )) .header("X-Device-Id", headerIdentifier) @@ -207,7 +200,6 @@ void findAllMemberDevices_NotFoundMember() { @DisplayName("인증되지 않은 디바이스로 조회 시 401 응답을 반환한다") void findAllMemberDevices_UnauthorizedDevice() { // given - final String email = "test@test.com"; final String headerIdentifier = "unauthorized-id"; given(memberService.findAllMemberDevices(any())) @@ -224,62 +216,17 @@ void findAllMemberDevices_UnauthorizedDevice() { .requestHeaders( headerWithName("X-Device-Id").description("디바이스 식별자") ) - .queryParameters( - parameterWithName("email").description("이메일") - ) .responseFields( fieldWithPath("message").type(JsonFieldType.STRING).description("에러 메시지") - ), - queryParameters( - parameterWithName("email").description("이메일") - ) + ) )) .header("X-Device-Id", headerIdentifier) - .param("email", email) - .when() .get("/api/v1/members") .then() .statusCode(HttpStatus.UNAUTHORIZED.value()) .body("message", equalTo("인증되지 않은 디바이스입니다")); } - @Test - @DisplayName("조회 시 유효하지 않은 이메일 형식인 경우 400 응답을 반환한다") - void findAllMemberDevices_InvalidEmailFormat() { - // given - final String invalidEmail = "invalid-email"; - final String headerIdentifier = "device-identifier"; - - // when - // then - given(this.spec) - .filter(document(DEFAULT_REST_DOC_PATH, - builder() - .tag("Member") - .summary("멤버 디바이스 조회") - .description("조회 시 유효하지 않은 이메일 형식인 경우 400 응답을 반환한다") - .requestHeaders( - headerWithName("X-Device-Id").description("디바이스 식별자") - ) - .queryParameters( - parameterWithName("email").description("이메일") - ) - .responseFields( - fieldWithPath("message").type(JsonFieldType.STRING).description("에러 메시지") - ), - queryParameters( - parameterWithName("email").description("이메일") - ) - )) - .header("X-Device-Id", headerIdentifier) - .param("email", invalidEmail) - .when() - .get("/api/v1/members") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()) - .body("message", equalTo("유효하지 않은 이메일 형식입니다.")); - } - @Test @DisplayName("이메일이 누락된 경우 400 응답을 반환한다") void saveMember_NullEmail() { @@ -369,7 +316,7 @@ void findAllMemberDevices_NotFoundDevice() { fieldWithPath("message").type(JsonFieldType.STRING).description("에러 메시지") ), queryParameters( - parameterWithName("email").description("이메일") + parameterWithName("email").description("이메일 (다음 버전에서 제거 예정)") ) )) .header("X-Device-Id", headerIdentifier) @@ -382,31 +329,28 @@ void findAllMemberDevices_NotFoundDevice() { } @Test - @DisplayName("이메일 파라미터가 누락된 경우 400 응답을 반환한다") + @DisplayName("이메일 파라미터가 누락되어도 200 응답을 반환한다") void findAllMemberDevices_NullEmail() { // given final String headerIdentifier = "device-identifier"; + final MemberFindOutput output = new MemberFindOutput( + Email.from("test@test.com"), + List.of(new MemberFindOutput.MemberFindElement( + DeviceIdentifier.from(headerIdentifier), + LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES) + )) + ); + given(memberService.findAllMemberDevices(any())).willReturn(output); // when // then given(this.spec) - .filter(document(DEFAULT_REST_DOC_PATH, - builder() - .tag("Member") - .summary("멤버 디바이스 조회") - .description("이메일 파라미터가 누락된 경우 400 응답을 반환한다") - .requestHeaders( - headerWithName("X-Device-Id").description("디바이스 식별자") - ) - .responseFields( - fieldWithPath("message").type(JsonFieldType.STRING).description("에러 메시지") - ) - )) .header("X-Device-Id", headerIdentifier) .when() .get("/api/v1/members") .then() - .statusCode(HttpStatus.BAD_REQUEST.value()); + .statusCode(HttpStatus.OK.value()) + .body("devices", hasSize(1)); } @Test @@ -444,7 +388,7 @@ void findAllMemberDevices_WithHeader() { headerWithName("X-Device-Id").description("디바이스 식별자") ) .queryParameters( - parameterWithName("email").description("이메일") + parameterWithName("email").description("이메일 (다음 버전에서 제거 예정)") ) .responseFields( fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"), @@ -455,7 +399,7 @@ void findAllMemberDevices_WithHeader() { .description("디바이스 생성일") ), queryParameters( - parameterWithName("email").description("이메일") + parameterWithName("email").description("이메일 (다음 버전에서 제거 예정)") ) )) .header("X-Device-Id", headerIdentifier) @@ -523,7 +467,7 @@ void findNotificationTime_NullNotificationTime() { ) .responseFields( fieldWithPath("notificationTime").type(JsonFieldType.STRING) - .description("알림 시간 (미설정 시 null)").optional() + .description("알림 시간 (설정되지 않은 경우 null)").optional() ) )) .header("X-Device-Id", headerIdentifier) diff --git a/src/test/java/com/recyclestudy/member/service/MemberServiceTest.java b/src/test/java/com/recyclestudy/member/service/MemberServiceTest.java index 7e950d0..1438e07 100644 --- a/src/test/java/com/recyclestudy/member/service/MemberServiceTest.java +++ b/src/test/java/com/recyclestudy/member/service/MemberServiceTest.java @@ -113,20 +113,22 @@ void saveDevice_existedMember() { @DisplayName("대상 이메일을 가진 멤버의 디바이스를 모두 조회한다") void findAllMemberDevices() { // given - final String email = "existed@test.com"; final DeviceIdentifier identifier = DeviceIdentifier.from("device-id"); - final MemberFindInput input = MemberFindInput.from(email, identifier); - final Member existedMember = Member.withoutId(input.email()); + final MemberFindInput input = MemberFindInput.from(identifier); + final Email email = Email.from("existed@test.com"); + final Member existedMember = Member.withoutId(email); final Device device = Device.withoutId(existedMember, input.deviceIdentifier(), true, ActivationExpiredDateTime.create(now)); - given(deviceRepository.findAllByMemberEmail(any(Email.class))).willReturn(List.of(device)); + given(memberRepository.findByIdentifier(identifier)).willReturn(Optional.of(existedMember)); + given(deviceRepository.findAllByMemberEmail(email)).willReturn(List.of(device)); // when final MemberFindOutput actual = memberService.findAllMemberDevices(input); // then assertSoftly(softAssertions -> { + softAssertions.assertThat(actual.email()).isEqualTo(email); softAssertions.assertThat(actual.elements()).hasSize(1); softAssertions.assertThat(actual.elements().getFirst().identifier()).isEqualTo(input.deviceIdentifier()); }); @@ -136,11 +138,12 @@ void findAllMemberDevices() { @DisplayName("대상 이메일을 가진 멤버의 디바이스가 없으면 빈 리스트를 리턴한다") void findAllMemberDevices_notExistedDevice() { // given - final String email = "existed@test.com"; final DeviceIdentifier identifier = DeviceIdentifier.from("device-id"); - final MemberFindInput input = MemberFindInput.from(email, identifier); + final MemberFindInput input = MemberFindInput.from(identifier); + final Member existedMember = Member.withoutId(Email.from("existed@test.com")); - given(deviceRepository.findAllByMemberEmail(any(Email.class))).willReturn(List.of()); + given(memberRepository.findByIdentifier(identifier)).willReturn(Optional.of(existedMember)); + given(deviceRepository.findAllByMemberEmail(existedMember.getEmail())).willReturn(List.of()); // when final MemberFindOutput actual = memberService.findAllMemberDevices(input); @@ -202,7 +205,7 @@ void authenticateDevice_not_existed_device() { // then assertThatThrownBy(() -> memberService.authenticateDevice(email, deviceIdentifier)) .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 디바이스 아이디입니다: %s".formatted(deviceIdentifier.getValue())); + .hasMessage("존재하지 않는 디바이스 식별자입니다: %s".formatted(deviceIdentifier.getValue())); } @Test @@ -222,24 +225,66 @@ void authenticateDevice_already_auth() { // then assertThatThrownBy(() -> memberService.authenticateDevice(otherEmail, deviceIdentifier)) .isInstanceOf(BadRequestException.class) - .hasMessage("이미 인증되었습니다"); + .hasMessage("이미 인증된 디바이스입니다"); } @Test @DisplayName("디바이스를 삭제할 수 있다") void deleteDevice() { // given - final Email email = Email.from("test@test.com"); final DeviceIdentifier deviceIdentifier = DeviceIdentifier.from("test"); final DeviceIdentifier targetDeviceIdentifier = DeviceIdentifier.from("target"); - final DeviceDeleteInput input = DeviceDeleteInput.from( - email.getValue(), deviceIdentifier, targetDeviceIdentifier.getValue()); + final DeviceDeleteInput input = DeviceDeleteInput.from(deviceIdentifier, targetDeviceIdentifier.getValue()); + final Member member = Member.withoutId(Email.from("test@test.com")); + final Device targetDevice = Device.withoutId(member, targetDeviceIdentifier, true, + ActivationExpiredDateTime.create(now)); + + given(memberRepository.findByIdentifier(deviceIdentifier)).willReturn(Optional.of(member)); + given(deviceRepository.findByIdentifier(targetDeviceIdentifier)).willReturn(Optional.of(targetDevice)); // when memberService.deleteDevice(input); // then - verify(deviceRepository).deleteByIdentifier(targetDeviceIdentifier); + verify(deviceRepository).delete(targetDevice); + } + + @Test + @DisplayName("다른 멤버가 소유한 디바이스를 삭제할 때 예외를 던진다") + void deleteDevice_notOwner() { + // given + final DeviceIdentifier deviceIdentifier = DeviceIdentifier.from("request-device"); + final DeviceIdentifier targetDeviceIdentifier = DeviceIdentifier.from("target-device"); + final DeviceDeleteInput input = DeviceDeleteInput.from(deviceIdentifier, targetDeviceIdentifier.getValue()); + + final Member requestMember = Member.withoutId(Email.from("request@test.com")); + final Member targetMember = Member.withoutId(Email.from("target@test.com")); + final Device targetDevice = Device.withoutId(targetMember, targetDeviceIdentifier, true, + ActivationExpiredDateTime.create(now)); + + given(memberRepository.findByIdentifier(deviceIdentifier)).willReturn(Optional.of(requestMember)); + given(deviceRepository.findByIdentifier(targetDeviceIdentifier)).willReturn(Optional.of(targetDevice)); + + // when + // then + assertThatThrownBy(() -> memberService.deleteDevice(input)) + .isInstanceOf(NotFoundException.class); + } + + @Test + @DisplayName("요청 디바이스가 유효하지 않을 때 디바이스 삭제 시 예외를 던진다") + void deleteDevice_unauthorized() { + // given + final DeviceIdentifier deviceIdentifier = DeviceIdentifier.from("request-device"); + final DeviceIdentifier targetDeviceIdentifier = DeviceIdentifier.from("target-device"); + final DeviceDeleteInput input = DeviceDeleteInput.from(deviceIdentifier, targetDeviceIdentifier.getValue()); + + given(memberRepository.findByIdentifier(deviceIdentifier)).willReturn(Optional.empty()); + + // when + // then + assertThatThrownBy(() -> memberService.deleteDevice(input)) + .isInstanceOf(UnauthorizedException.class); } @Test