diff --git a/src/main/java/com/dasom/MemoReal/MemoRealApplication.java b/src/main/java/com/dasom/MemoReal/MemoRealApplication.java index b0e38cd..9f54c8f 100644 --- a/src/main/java/com/dasom/MemoReal/MemoRealApplication.java +++ b/src/main/java/com/dasom/MemoReal/MemoRealApplication.java @@ -2,8 +2,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication +// createAt, updatedAt 자동 빌드를 위한 어노테이션 +@EnableJpaAuditing public class MemoRealApplication { public static void main(String[] args) { diff --git a/src/main/java/com/dasom/MemoReal/domain/capsule/controller/CapsuleController.java b/src/main/java/com/dasom/MemoReal/domain/capsule/controller/CapsuleController.java new file mode 100644 index 0000000..d487709 --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/capsule/controller/CapsuleController.java @@ -0,0 +1,77 @@ +package com.dasom.MemoReal.domain.capsule.controller; + +import com.dasom.MemoReal.domain.capsule.dto.CapsuleRequestDto; +import com.dasom.MemoReal.domain.capsule.dto.CapsuleResponseDto; +import com.dasom.MemoReal.domain.capsule.service.CapsuleService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/capsules") +public class CapsuleController { + + private final CapsuleService capsuleService; + + @Operation(summary = "타임캡슐 생성", description = "새 타임캡슐을 생성") + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "타임캡슐 생성 성공"), + @ApiResponse(responseCode = "400", description = "입력값 오류") + }) + @PostMapping + public ResponseEntity createCapsule( + @RequestBody CapsuleRequestDto requestDto + ) { + return ResponseEntity.status(HttpStatus.CREATED).body(capsuleService.createCapsule(requestDto)); + } + + @Operation(summary = "타임캡슐 단건 조회", description = "ID기반 특정 타임캡슐 조회") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "타임캡슐 조회 성공"), + @ApiResponse(responseCode = "404", description = "해당 ID의 타임캡슐이 존재하지 않음") + }) + @GetMapping("/{id}") + public ResponseEntity getCapsuleById(@PathVariable("id") Long id) { + return ResponseEntity.ok(capsuleService.getCapsule(id)); + } + + @Operation(summary = "모든 타임캡슐 조회", description = "모든 타임캡슐 목록 조회") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "타임캡슐 목록 조회 성공") + }) + @GetMapping + public ResponseEntity> getAllCapsules() { + return ResponseEntity.ok(capsuleService.getAllCapsules()); + } + + @Operation(summary = "타임캡슐 수정", description = "ID기반 타임캡슐 정보 수정") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "타임캡슐 수정 성공"), + @ApiResponse(responseCode = "404", description = "해당 ID의 타임캡슐이 존재하지 않음") + }) + @PutMapping("/{id}") + public ResponseEntity updateCapsule( + @PathVariable("id") Long id, + @RequestBody CapsuleRequestDto requestDto + ) { + return ResponseEntity.ok(capsuleService.updateCapsule(id, requestDto)); + } + + @Operation(summary = "타임캡슐 삭제", description = "ID기반 타임캡슐 삭제") + @ApiResponses({ + @ApiResponse(responseCode = "204", description = "타임캡슐 삭제 성공"), + @ApiResponse(responseCode = "404", description = "해당 ID의 타임캡슐이 존재하지 않음") + }) + @DeleteMapping("/{id}") + public ResponseEntity deleteCapsule(@PathVariable("id") Long id) { + capsuleService.deleteCapsule(id); + return ResponseEntity.noContent().build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/dasom/MemoReal/domain/capsule/dto/CapsuleRequestDto.java b/src/main/java/com/dasom/MemoReal/domain/capsule/dto/CapsuleRequestDto.java new file mode 100644 index 0000000..82d1bad --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/capsule/dto/CapsuleRequestDto.java @@ -0,0 +1,20 @@ +package com.dasom.MemoReal.domain.capsule.dto; + +import com.dasom.MemoReal.domain.capsule.type.CapsuleType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CapsuleRequestDto { + private String title; + private CapsuleType type; + private String content; + private LocalDate openDate; +} \ No newline at end of file diff --git a/src/main/java/com/dasom/MemoReal/domain/capsule/dto/CapsuleResponseDto.java b/src/main/java/com/dasom/MemoReal/domain/capsule/dto/CapsuleResponseDto.java new file mode 100644 index 0000000..f9d0632 --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/capsule/dto/CapsuleResponseDto.java @@ -0,0 +1,29 @@ +package com.dasom.MemoReal.domain.capsule.dto; + +import com.dasom.MemoReal.domain.capsule.type.CapsuleType; +import com.dasom.MemoReal.domain.user.dto.UserDTO; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CapsuleResponseDto { + private Long id; + private String title; + private CapsuleType type; + private String content; + private LocalDate openDate; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private List medias; + + private UserDTO user; // 회원 정보 추가 +} \ No newline at end of file diff --git a/src/main/java/com/dasom/MemoReal/domain/capsule/dto/MediaDto.java b/src/main/java/com/dasom/MemoReal/domain/capsule/dto/MediaDto.java new file mode 100644 index 0000000..969c146 --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/capsule/dto/MediaDto.java @@ -0,0 +1,18 @@ +package com.dasom.MemoReal.domain.capsule.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MediaDto { + + private Long id; + private String cid; + private String originalFileName; + +} \ No newline at end of file diff --git a/src/main/java/com/dasom/MemoReal/domain/capsule/entity/Capsule.java b/src/main/java/com/dasom/MemoReal/domain/capsule/entity/Capsule.java new file mode 100644 index 0000000..1a5f19f --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/capsule/entity/Capsule.java @@ -0,0 +1,100 @@ +package com.dasom.MemoReal.domain.capsule.entity; + +import com.dasom.MemoReal.domain.capsule.dto.CapsuleRequestDto; +import com.dasom.MemoReal.domain.capsule.dto.CapsuleResponseDto; +import com.dasom.MemoReal.domain.capsule.dto.MediaDto; +import com.dasom.MemoReal.domain.capsule.type.CapsuleType; +import com.dasom.MemoReal.domain.user.dto.UserDTO; +import com.dasom.MemoReal.domain.user.entity.User; +import com.dasom.MemoReal.global.exception.CustomException; +import com.dasom.MemoReal.global.exception.ErrorCode; +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Table(name = "capsules") +@EntityListeners(AuditingEntityListener.class) +@Builder +public class Capsule { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + + @Enumerated(EnumType.STRING) + private CapsuleType type; + + private String content; + + @Column(nullable = false) + private LocalDate openDate; + + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; + + @OneToMany(mappedBy = "capsule", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default + private List medias = new ArrayList<>(); + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + public static Capsule toEntity(CapsuleRequestDto dto, User user) { + if (user == null) { + throw new CustomException(ErrorCode.USER_NOT_FOUND); + } + return Capsule.builder() + .title(dto.getTitle()) + .type(dto.getType()) + .content(dto.getContent()) + .openDate(dto.getOpenDate()) + .user(user) + .build(); + } + + public static CapsuleResponseDto toDto(Capsule capsule) { + List mediaDtos = (capsule.getMedias() != null) ? + capsule.getMedias().stream() + .map(Media::toDto) + .collect(Collectors.toList()) : + new ArrayList<>(); + + return CapsuleResponseDto.builder() + .id(capsule.getId()) + .title(capsule.getTitle()) + .type(capsule.getType()) + .content(capsule.getContent()) + .openDate(capsule.getOpenDate()) + .createdAt(capsule.getCreatedAt()) + .updatedAt(capsule.getUpdatedAt()) + .medias(mediaDtos) + .user(UserDTO.toDto(capsule.getUser())) + .build(); + } + + public void update(CapsuleRequestDto requestDto) { + this.title = requestDto.getTitle(); + this.type = requestDto.getType(); + this.content = requestDto.getContent(); + this.openDate = requestDto.getOpenDate(); + } +} \ No newline at end of file diff --git a/src/main/java/com/dasom/MemoReal/domain/capsule/entity/Media.java b/src/main/java/com/dasom/MemoReal/domain/capsule/entity/Media.java new file mode 100644 index 0000000..5f0845d --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/capsule/entity/Media.java @@ -0,0 +1,38 @@ +package com.dasom.MemoReal.domain.capsule.entity; + +import com.dasom.MemoReal.domain.capsule.dto.MediaDto; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@AllArgsConstructor +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "medias") +public class Media { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String cid; // IPFS CID + private String originalFileName; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "capsule_id") + private Capsule capsule; + + public static MediaDto toDto(Media media) { + if (media == null) { + return MediaDto.builder().build(); + } + + return MediaDto.builder() + .id(media.getId()) + .cid(media.getCid()) + .originalFileName(media.getOriginalFileName()) + .build(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/dasom/MemoReal/domain/capsule/repository/CapsuleRepository.java b/src/main/java/com/dasom/MemoReal/domain/capsule/repository/CapsuleRepository.java new file mode 100644 index 0000000..a92c912 --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/capsule/repository/CapsuleRepository.java @@ -0,0 +1,13 @@ +package com.dasom.MemoReal.domain.capsule.repository; + +import com.dasom.MemoReal.domain.capsule.entity.Capsule; +import com.dasom.MemoReal.domain.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface CapsuleRepository extends JpaRepository { + List findByUser(User user); +} \ No newline at end of file diff --git a/src/main/java/com/dasom/MemoReal/domain/capsule/service/CapsuleService.java b/src/main/java/com/dasom/MemoReal/domain/capsule/service/CapsuleService.java new file mode 100644 index 0000000..0bc0aea --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/capsule/service/CapsuleService.java @@ -0,0 +1,109 @@ +package com.dasom.MemoReal.domain.capsule.service; + +import com.dasom.MemoReal.domain.capsule.dto.CapsuleRequestDto; +import com.dasom.MemoReal.domain.capsule.dto.CapsuleResponseDto; +import com.dasom.MemoReal.domain.capsule.entity.Capsule; +import com.dasom.MemoReal.domain.capsule.repository.CapsuleRepository; +import com.dasom.MemoReal.domain.user.entity.User; +import com.dasom.MemoReal.domain.user.repository.UserRepository; +import com.dasom.MemoReal.global.exception.CustomException; +import com.dasom.MemoReal.global.exception.ErrorCode; +import com.dasom.MemoReal.global.security.util.SecurityUtil; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + + +@Service +@RequiredArgsConstructor +@Transactional +public class CapsuleService { + + private final CapsuleRepository capsuleRepository; + private final UserRepository userRepository; + + /** + * 새로운 캡슐을 생성합니다. + */ + public CapsuleResponseDto createCapsule(CapsuleRequestDto requestDto) { + String currentUserEmail = SecurityUtil.getCurrentUsername(); // CustomException(UNAUTHORIZED) 발생 가능 + User currentUser = userRepository.findByEmail(currentUserEmail) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + Capsule capsule = Capsule.toEntity(requestDto, currentUser); + Capsule savedCapsule = capsuleRepository.save(capsule); + return Capsule.toDto(savedCapsule); + } + + /** + * 특정 ID의 캡슐을 조회합니다. + */ + @Transactional(readOnly = true) + public CapsuleResponseDto getCapsule(Long id) { + String currentUserEmail = SecurityUtil.getCurrentUsername(); // CustomException(UNAUTHORIZED) 발생 가능 + Capsule capsule = findCapsuleById(id); + + if (!capsule.getUser().getEmail().equals(currentUserEmail)) { + throw new CustomException(ErrorCode.UNAUTHORIZED); + } + return Capsule.toDto(capsule); + } + + /** + * 현재 사용자가 소유한 모든 캡슐을 조회합니다. + */ + @Transactional(readOnly = true) + public List getAllCapsules() { + String currentUserEmail = SecurityUtil.getCurrentUsername(); // CustomException(UNAUTHORIZED) 발생 가능 + User currentUser = userRepository.findByEmail(currentUserEmail) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + return capsuleRepository.findByUser(currentUser).stream() + .map(Capsule::toDto) + .collect(Collectors.toList()); + } + + /** + * 특정 캡슐을 수정합니다. + */ + public CapsuleResponseDto updateCapsule(Long id, CapsuleRequestDto requestDto) { + String currentUserEmail = SecurityUtil.getCurrentUsername(); // CustomException(UNAUTHORIZED) 발생 가능 + Capsule capsule = findCapsuleById(id); + + if (!capsule.getUser().getEmail().equals(currentUserEmail)) { + throw new CustomException(ErrorCode.UNAUTHORIZED); + } + + capsule.update(requestDto); // requestDto 객체 자체를 전달합니다. + + Capsule savedCapsule = capsuleRepository.save(capsule); + return Capsule.toDto(savedCapsule); + } + + /** + * 특정 캡슐을 삭제합니다. + */ + public void deleteCapsule(Long id) { + String currentUserEmail = SecurityUtil.getCurrentUsername(); // CustomException(UNAUTHORIZED) 발생 가능 + Capsule capsule = findCapsuleById(id); + + if (!capsule.getUser().getEmail().equals(currentUserEmail)) { + throw new CustomException(ErrorCode.UNAUTHORIZED); + } + + capsuleRepository.delete(capsule); + } + + /** + * ID를 통해 캡슐을 찾고, 없을 경우 예외를 발생시킵니다. + */ + private Capsule findCapsuleById(Long id) { + return capsuleRepository.findById(id) + .orElseThrow(() -> new CustomException(ErrorCode.CAPSULE_NOT_FOUND)); + } +} \ No newline at end of file diff --git a/src/main/java/com/dasom/MemoReal/domain/capsule/type/CapsuleType.java b/src/main/java/com/dasom/MemoReal/domain/capsule/type/CapsuleType.java new file mode 100644 index 0000000..838d160 --- /dev/null +++ b/src/main/java/com/dasom/MemoReal/domain/capsule/type/CapsuleType.java @@ -0,0 +1,6 @@ +package com.dasom.MemoReal.domain.capsule.type; + +public enum CapsuleType { + NORMAL, + TIME +} diff --git a/src/main/java/com/dasom/MemoReal/global/exception/ErrorCode.java b/src/main/java/com/dasom/MemoReal/global/exception/ErrorCode.java index cca25cc..f9dcd22 100644 --- a/src/main/java/com/dasom/MemoReal/global/exception/ErrorCode.java +++ b/src/main/java/com/dasom/MemoReal/global/exception/ErrorCode.java @@ -18,7 +18,11 @@ public enum ErrorCode { // 일반적인 에러(유효성 검사 등) INVALID_INPUT_VALUE("COMMON_001", HttpStatus.BAD_REQUEST, "유효하지 않은 입력 값입니다."), UNAUTHORIZED("COMMON_002", HttpStatus.UNAUTHORIZED, "인증되지 않은 접근입니다."), - INTERNAL_SERVER_ERROR("COMMON_999", HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다."); + INTERNAL_SERVER_ERROR("COMMON_999", HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다."), + + // 캡슐 관련 오류 + CAPSULE_NOT_FOUND("CAPSULE_001", HttpStatus.NOT_FOUND, "해당 캡슐을 찾을 수 없습니다."), + INVALID_CAPSULE_REQUEST("CAPSULE_002", HttpStatus.BAD_REQUEST, "잘못된 요청입니다."); private final String code; private final HttpStatus httpStatus; diff --git a/src/main/java/com/dasom/MemoReal/global/exception/GlobalExceptionHandler.java b/src/main/java/com/dasom/MemoReal/global/exception/GlobalExceptionHandler.java index a4cef23..982d312 100644 --- a/src/main/java/com/dasom/MemoReal/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/dasom/MemoReal/global/exception/GlobalExceptionHandler.java @@ -1,44 +1,43 @@ package com.dasom.MemoReal.global.exception; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.springframework.web.multipart.MaxUploadSizeExceededException; +import java.io.IOException; import java.util.Map; - @RestControllerAdvice public class GlobalExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); - // 사용자 정의 예외 처리 (비즈니스, 서버 예외) + // 사용자 정의 예외 처리 (캡슐 관련 포함) @ExceptionHandler(CustomException.class) protected ResponseEntity handleCustomException(CustomException e) { ErrorCode code = e.getErrorCode(); - HttpStatus status = code.getHttpStatus(); log.warn("CustomException occurred: {}", code.getMessage(), e); return ResponseEntity - .status(status) + .status(code.getHttpStatus()) .body(Map.of( "success", false, "error", code.getMessage() - )); // 오류라 success=false와 에러 메시지 전달 // 오류라 success=false + )); } - // 예기치 못한 예외 처리 + // 예기치 못한 모든 예외 처리 @ExceptionHandler(Exception.class) - protected ResponseEntity handleException(Exception e) { + protected ResponseEntity handleUnhandledException(Exception e) { log.error("Unhandled Exception: {}", e.getMessage(), e); return ResponseEntity - .status(ErrorCode.INTERNAL_SERVER_ERROR.getHttpStatus()) + .status(HttpStatus.INTERNAL_SERVER_ERROR) .body(Map.of( "success", false, "error", "Internal Server Error" - ));// 오류라 success=false + )); } - } \ No newline at end of file diff --git a/src/test/java/com/dasom/MemoReal/domain/capsule/CapsuleServiceTest.java b/src/test/java/com/dasom/MemoReal/domain/capsule/CapsuleServiceTest.java new file mode 100644 index 0000000..b3472bd --- /dev/null +++ b/src/test/java/com/dasom/MemoReal/domain/capsule/CapsuleServiceTest.java @@ -0,0 +1,519 @@ +package com.dasom.MemoReal.domain.capsule; + +import com.dasom.MemoReal.domain.capsule.dto.CapsuleRequestDto; +import com.dasom.MemoReal.domain.capsule.dto.CapsuleResponseDto; +import com.dasom.MemoReal.domain.capsule.entity.Capsule; +import com.dasom.MemoReal.domain.capsule.repository.CapsuleRepository; +import com.dasom.MemoReal.domain.capsule.service.CapsuleService; +import com.dasom.MemoReal.domain.capsule.type.CapsuleType; +import com.dasom.MemoReal.domain.user.entity.User; +import com.dasom.MemoReal.domain.user.repository.UserRepository; +import com.dasom.MemoReal.global.exception.CustomException; +import com.dasom.MemoReal.global.exception.ErrorCode; +import com.dasom.MemoReal.global.security.util.SecurityUtil; // SecurityUtil 임포트 필요! +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; // MockedStatic 임포트 +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.anyLong; // anyLong()을 위해 추가 +import static org.mockito.Mockito.*; + +@MockitoSettings(strictness = Strictness.LENIENT) +@ExtendWith(MockitoExtension.class) +public class CapsuleServiceTest { + + @Mock + private CapsuleRepository capsuleRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private Authentication authentication; + @Mock + private SecurityContext securityContext; + + @InjectMocks + private CapsuleService capsuleService; + + private User testUser; + private Capsule testCapsule; + private CapsuleRequestDto testRequestDto; + + @BeforeEach + void setUp() { + when(securityContext.getAuthentication()).thenReturn(authentication); + SecurityContextHolder.setContext(securityContext); + + testUser = User.builder() + .id(1L) + .email("test@example.com") + .username("testuser") + .password("password") + .build(); + + testCapsule = Capsule.builder() + .id(1L) + .title("테스트 캡슐") + .type(CapsuleType.NORMAL) + .content("테스트 내용") + .openDate(LocalDate.now().plusDays(7)) + .user(testUser) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .medias(new ArrayList<>()) + .build(); + + testRequestDto = CapsuleRequestDto.builder() + .title("새 캡슐 제목") + .type(CapsuleType.NORMAL) + .content("새 캡슐 내용") + .openDate(LocalDate.now().plusMonths(1)) + .build(); + + // 일반적으로 userRepository는 testUser를 반환하도록 설정 + when(userRepository.findByEmail(testUser.getEmail())).thenReturn(Optional.of(testUser)); + } + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + @DisplayName("캡슐 생성 성공") + void createCapsule_Success() { + try (MockedStatic mockedSecurityUtil = mockStatic(SecurityUtil.class)) { + mockedSecurityUtil.when(SecurityUtil::getCurrentUsername) + .thenReturn(testUser.getEmail()); // SecurityUtil 정상 동작 Mocking + + ArgumentCaptor capsuleCaptor = ArgumentCaptor.forClass(Capsule.class); + + when(capsuleRepository.save(capsuleCaptor.capture())).thenAnswer(invocation -> { + Capsule capturedCapsule = invocation.getArgument(0); + return Capsule.builder() + .id(1L) + .title(capturedCapsule.getTitle()) + .type(capturedCapsule.getType()) + .content(capturedCapsule.getContent()) + .openDate(capturedCapsule.getOpenDate()) + .user(capturedCapsule.getUser()) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .medias(new ArrayList<>()) + .build(); + }); + + CapsuleResponseDto responseDto = capsuleService.createCapsule(testRequestDto); + + assertThat(responseDto).isNotNull(); + assertThat(responseDto.getTitle()).isEqualTo(testRequestDto.getTitle()); + assertThat(responseDto.getType()).isEqualTo(testRequestDto.getType()); + assertThat(responseDto.getContent()).isEqualTo(testRequestDto.getContent()); + assertThat(responseDto.getOpenDate()).isEqualTo(testRequestDto.getOpenDate()); + assertThat(responseDto.getUser().getEmail()).isEqualTo(testUser.getEmail()); + + verify(capsuleRepository, times(1)).save(any(Capsule.class)); + mockedSecurityUtil.verify(SecurityUtil::getCurrentUsername, times(1)); + } + } + + @Test + @DisplayName("단일 캡슐 조회 성공") + void getCapsule_Success() { + try (MockedStatic mockedSecurityUtil = mockStatic(SecurityUtil.class)) { + mockedSecurityUtil.when(SecurityUtil::getCurrentUsername) + .thenReturn(testUser.getEmail()); // SecurityUtil 정상 동작 Mocking + + Long capsuleId = 1L; + when(capsuleRepository.findById(capsuleId)).thenReturn(Optional.of(testCapsule)); + + CapsuleResponseDto responseDto = capsuleService.getCapsule(capsuleId); + + assertThat(responseDto).isNotNull(); + assertThat(responseDto.getTitle()).isEqualTo(testCapsule.getTitle()); + assertThat(responseDto.getUser().getEmail()).isEqualTo(testUser.getEmail()); + verify(capsuleRepository, times(1)).findById(capsuleId); + mockedSecurityUtil.verify(SecurityUtil::getCurrentUsername, times(1)); + } + } + + @Test + @DisplayName("단일 캡슐 조회 실패 - 캡슐을 찾을 수 없음") + void getCapsule_NotFound() { + try (MockedStatic mockedSecurityUtil = mockStatic(SecurityUtil.class)) { + mockedSecurityUtil.when(SecurityUtil::getCurrentUsername) + .thenReturn(testUser.getEmail()); // SecurityUtil 정상 동작 Mocking + + Long nonExistentCapsuleId = 99L; + when(capsuleRepository.findById(nonExistentCapsuleId)).thenReturn(Optional.empty()); + + CustomException exception = assertThrows(CustomException.class, () -> + capsuleService.getCapsule(nonExistentCapsuleId) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.CAPSULE_NOT_FOUND); + verify(capsuleRepository, times(1)).findById(nonExistentCapsuleId); + mockedSecurityUtil.verify(SecurityUtil::getCurrentUsername, times(1)); + } + } + + @Test + @DisplayName("단일 캡슐 조회 실패 - 권한 없음 (다른 사용자의 캡슐)") + void getCapsule_Unauthorized() { + try (MockedStatic mockedSecurityUtil = mockStatic(SecurityUtil.class)) { + mockedSecurityUtil.when(SecurityUtil::getCurrentUsername) + .thenReturn(testUser.getEmail()); // SecurityUtil 정상 동작 Mocking + + Long capsuleId = 1L; + User otherUser = User.builder().id(2L).email("other@example.com").username("otheruser").password("pass").build(); + Capsule otherUserCapsule = Capsule.builder() + .id(1L) + .title("다른 사용자 캡슐") + .type(CapsuleType.NORMAL) + .content("다른 사용자 내용") + .openDate(LocalDate.now().plusDays(10)) + .user(otherUser) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .medias(new ArrayList<>()) + .build(); + + when(capsuleRepository.findById(capsuleId)).thenReturn(Optional.of(otherUserCapsule)); + + CustomException exception = assertThrows(CustomException.class, () -> + capsuleService.getCapsule(capsuleId) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.UNAUTHORIZED); + verify(capsuleRepository, times(1)).findById(capsuleId); + mockedSecurityUtil.verify(SecurityUtil::getCurrentUsername, times(1)); + } + } + + @Test + @DisplayName("모든 캡슐 조회 성공 (현재 사용자 캡슐만)") + void getAllCapsules_Success() { + try (MockedStatic mockedSecurityUtil = mockStatic(SecurityUtil.class)) { + mockedSecurityUtil.when(SecurityUtil::getCurrentUsername) + .thenReturn(testUser.getEmail()); // SecurityUtil 정상 동작 Mocking + + Capsule anotherCapsule = Capsule.builder() + .id(2L) + .title("다른 내 캡슐") + .type(CapsuleType.NORMAL) + .content("내용2") + .openDate(LocalDate.now().plusDays(14)) + .user(testUser) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .medias(new ArrayList<>()) + .build(); + + List userCapsules = Arrays.asList(testCapsule, anotherCapsule); + when(capsuleRepository.findByUser(testUser)).thenReturn(userCapsules); + + List responseDtos = capsuleService.getAllCapsules(); + + assertThat(responseDtos).isNotNull(); + assertThat(responseDtos).hasSize(2); + assertThat(responseDtos.get(0).getTitle()).isEqualTo(testCapsule.getTitle()); + assertThat(responseDtos.get(1).getTitle()).isEqualTo(anotherCapsule.getTitle()); + responseDtos.forEach(dto -> { + assertThat(dto.getUser().getEmail()).isEqualTo(testUser.getEmail()); + }); + verify(capsuleRepository, times(1)).findByUser(testUser); + mockedSecurityUtil.verify(SecurityUtil::getCurrentUsername, times(1)); + } + } + + @Test + @DisplayName("캡슐 업데이트 성공") + void updateCapsule_Success() { + try (MockedStatic mockedSecurityUtil = mockStatic(SecurityUtil.class)) { + mockedSecurityUtil.when(SecurityUtil::getCurrentUsername) + .thenReturn(testUser.getEmail()); // SecurityUtil 정상 동작 Mocking + + Long capsuleId = 1L; + when(capsuleRepository.findById(capsuleId)).thenReturn(Optional.of(testCapsule)); + when(capsuleRepository.save(any(Capsule.class))).thenReturn(testCapsule); + + CapsuleRequestDto updateRequestDto = CapsuleRequestDto.builder() + .title("업데이트된 제목") + .type(CapsuleType.TIME) + .content("업데이트된 내용") + .openDate(LocalDate.now().plusDays(30)) + .build(); + + CapsuleResponseDto responseDto = capsuleService.updateCapsule(capsuleId, updateRequestDto); + + assertThat(responseDto).isNotNull(); + assertThat(responseDto.getTitle()).isEqualTo(updateRequestDto.getTitle()); + assertThat(responseDto.getType()).isEqualTo(updateRequestDto.getType()); + assertThat(responseDto.getContent()).isEqualTo(updateRequestDto.getContent()); + assertThat(responseDto.getOpenDate()).isEqualTo(updateRequestDto.getOpenDate()); + assertThat(responseDto.getUser().getEmail()).isEqualTo(testUser.getEmail()); + verify(capsuleRepository, times(1)).findById(capsuleId); + verify(capsuleRepository, times(1)).save(any(Capsule.class)); + mockedSecurityUtil.verify(SecurityUtil::getCurrentUsername, times(1)); + } + } + + @Test + @DisplayName("캡슐 업데이트 실패 - 캡슐을 찾을 수 없음") + void updateCapsule_NotFound() { + try (MockedStatic mockedSecurityUtil = mockStatic(SecurityUtil.class)) { + mockedSecurityUtil.when(SecurityUtil::getCurrentUsername) + .thenReturn(testUser.getEmail()); // SecurityUtil 정상 동작 Mocking + + Long nonExistentCapsuleId = 99L; + when(capsuleRepository.findById(nonExistentCapsuleId)).thenReturn(Optional.empty()); + + CustomException exception = assertThrows(CustomException.class, () -> + capsuleService.updateCapsule(nonExistentCapsuleId, testRequestDto) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.CAPSULE_NOT_FOUND); + verify(capsuleRepository, times(1)).findById(nonExistentCapsuleId); + verify(capsuleRepository, never()).save(any(Capsule.class)); + mockedSecurityUtil.verify(SecurityUtil::getCurrentUsername, times(1)); + } + } + + @Test + @DisplayName("캡슐 업데이트 실패 - 권한 없음") + void updateCapsule_Unauthorized() { + try (MockedStatic mockedSecurityUtil = mockStatic(SecurityUtil.class)) { + mockedSecurityUtil.when(SecurityUtil::getCurrentUsername) + .thenReturn(testUser.getEmail()); // SecurityUtil 정상 동작 Mocking + + Long capsuleId = 1L; + User otherUser = User.builder().id(2L).email("other@example.com").username("otheruser").password("pass").build(); + Capsule otherUserCapsule = Capsule.builder() + .id(1L) + .title("다른 사용자 캡슐") + .type(CapsuleType.TIME) + .content("다른 사용자 내용") + .openDate(LocalDate.now().plusDays(10)) + .user(otherUser) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .medias(new ArrayList<>()) + .build(); + + when(capsuleRepository.findById(capsuleId)).thenReturn(Optional.of(otherUserCapsule)); + + CustomException exception = assertThrows(CustomException.class, () -> + capsuleService.updateCapsule(capsuleId, testRequestDto) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.UNAUTHORIZED); + verify(capsuleRepository, times(1)).findById(capsuleId); + verify(capsuleRepository, never()).save(any(Capsule.class)); + mockedSecurityUtil.verify(SecurityUtil::getCurrentUsername, times(1)); + } + } + + @Test + @DisplayName("캡슐 삭제 성공") + void deleteCapsule_Success() { + try (MockedStatic mockedSecurityUtil = mockStatic(SecurityUtil.class)) { + mockedSecurityUtil.when(SecurityUtil::getCurrentUsername) + .thenReturn(testUser.getEmail()); // SecurityUtil 정상 동작 Mocking + + Long capsuleId = 1L; + when(capsuleRepository.findById(capsuleId)).thenReturn(Optional.of(testCapsule)); + doNothing().when(capsuleRepository).delete(any(Capsule.class)); + + capsuleService.deleteCapsule(capsuleId); + + verify(capsuleRepository, times(1)).findById(capsuleId); + verify(capsuleRepository, times(1)).delete(testCapsule); + mockedSecurityUtil.verify(SecurityUtil::getCurrentUsername, times(1)); + } + } + + @Test + @DisplayName("캡슐 삭제 실패 - 캡슐을 찾을 수 없음") + void deleteCapsule_NotFound() { + try (MockedStatic mockedSecurityUtil = mockStatic(SecurityUtil.class)) { + mockedSecurityUtil.when(SecurityUtil::getCurrentUsername) + .thenReturn(testUser.getEmail()); // SecurityUtil 정상 동작 Mocking + + Long nonExistentCapsuleId = 99L; + when(capsuleRepository.findById(nonExistentCapsuleId)).thenReturn(Optional.empty()); + + CustomException exception = assertThrows(CustomException.class, () -> + capsuleService.deleteCapsule(nonExistentCapsuleId) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.CAPSULE_NOT_FOUND); + verify(capsuleRepository, times(1)).findById(nonExistentCapsuleId); + verify(capsuleRepository, never()).delete(any(Capsule.class)); + mockedSecurityUtil.verify(SecurityUtil::getCurrentUsername, times(1)); + } + } + + @Test + @DisplayName("캡슐 삭제 실패 - 권한 없음") + void deleteCapsule_Unauthorized() { + try (MockedStatic mockedSecurityUtil = mockStatic(SecurityUtil.class)) { + mockedSecurityUtil.when(SecurityUtil::getCurrentUsername) + .thenReturn(testUser.getEmail()); // SecurityUtil 정상 동작 Mocking + + Long capsuleId = 1L; + User otherUser = User.builder().id(2L).email("other@example.com").username("otheruser").password("pass").build(); + Capsule otherUserCapsule = Capsule.builder() + .id(1L) + .title("다른 사용자 캡슐") + .type(CapsuleType.TIME) + .content("다른 사용자 내용") + .openDate(LocalDate.now().plusDays(10)) + .user(otherUser) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .medias(new ArrayList<>()) + .build(); + + when(capsuleRepository.findById(capsuleId)).thenReturn(Optional.of(otherUserCapsule)); + + CustomException exception = assertThrows(CustomException.class, () -> + capsuleService.deleteCapsule(capsuleId) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.UNAUTHORIZED); + verify(capsuleRepository, times(1)).findById(capsuleId); + verify(capsuleRepository, never()).delete(any(Capsule.class)); + mockedSecurityUtil.verify(SecurityUtil::getCurrentUsername, times(1)); + } + } + + // --- 새로 추가되거나 수정된 테스트 --- + + @Test + @DisplayName("현재 사용자 정보 없을 시 RuntimeException 발생 - 캡슐 생성") + void createCapsule_SecurityUtilThrowsRuntimeException() { + try (MockedStatic mockedSecurityUtil = mockStatic(SecurityUtil.class)) { + // SecurityUtil.getCurrentUsername()이 RuntimeException을 던지도록 모킹 + mockedSecurityUtil.when(SecurityUtil::getCurrentUsername) + .thenThrow(new RuntimeException("인증되지 않은 접근입니다.")); // 메시지 수정 + + // CapsuleService는 이 RuntimeException을 그대로 전파해야 함 + RuntimeException exception = assertThrows(RuntimeException.class, () -> + capsuleService.createCapsule(testRequestDto) + ); + + assertThat(exception.getMessage()).isEqualTo("인증되지 않은 접근입니다."); // 메시지 수정 + mockedSecurityUtil.verify(SecurityUtil::getCurrentUsername, times(1)); + verify(userRepository, never()).findByEmail(anyString()); // 예외 발생 시 호출되면 안 됨 + verify(capsuleRepository, never()).save(any(Capsule.class)); // 예외 발생 시 호출되면 안 됨 + } + } + + @Test + @DisplayName("현재 사용자 정보 없을 시 RuntimeException 발생 - 캡슐 조회") + void getCapsule_SecurityUtilThrowsRuntimeException() { + try (MockedStatic mockedSecurityUtil = mockStatic(SecurityUtil.class)) { + mockedSecurityUtil.when(SecurityUtil::getCurrentUsername) + .thenThrow(new RuntimeException("인증되지 않은 접근입니다.")); // 메시지 수정 + + RuntimeException exception = assertThrows(RuntimeException.class, () -> + capsuleService.getCapsule(1L) + ); + + assertThat(exception.getMessage()).isEqualTo("인증되지 않은 접근입니다."); // 메시지 수정 + mockedSecurityUtil.verify(SecurityUtil::getCurrentUsername, times(1)); + verify(capsuleRepository, never()).findById(anyLong()); + } + } + + @Test + @DisplayName("현재 사용자 정보 없을 시 RuntimeException 발생 - 모든 캡슐 조회") + void getAllCapsules_SecurityUtilThrowsRuntimeException() { + try (MockedStatic mockedSecurityUtil = mockStatic(SecurityUtil.class)) { + mockedSecurityUtil.when(SecurityUtil::getCurrentUsername) + .thenThrow(new RuntimeException("인증되지 않은 접근입니다.")); // 메시지 수정 + + RuntimeException exception = assertThrows(RuntimeException.class, () -> + capsuleService.getAllCapsules() + ); + + assertThat(exception.getMessage()).isEqualTo("인증되지 않은 접근입니다."); // 메시지 수정 + mockedSecurityUtil.verify(SecurityUtil::getCurrentUsername, times(1)); + verify(userRepository, never()).findByEmail(anyString()); + verify(capsuleRepository, never()).findByUser(any(User.class)); + } + } + + @Test + @DisplayName("현재 사용자 정보 없을 시 RuntimeException 발생 - 캡슐 업데이트") + void updateCapsule_SecurityUtilThrowsRuntimeException() { + try (MockedStatic mockedSecurityUtil = mockStatic(SecurityUtil.class)) { + mockedSecurityUtil.when(SecurityUtil::getCurrentUsername) + .thenThrow(new RuntimeException("인증되지 않은 접근입니다.")); // 메시지 수정 + + RuntimeException exception = assertThrows(RuntimeException.class, () -> + capsuleService.updateCapsule(1L, testRequestDto) + ); + + assertThat(exception.getMessage()).isEqualTo("인증되지 않은 접근입니다."); // 메시지 수정 + mockedSecurityUtil.verify(SecurityUtil::getCurrentUsername, times(1)); + verify(capsuleRepository, never()).findById(anyLong()); + verify(capsuleRepository, never()).save(any(Capsule.class)); + } + } + + @Test + @DisplayName("현재 사용자 정보 없을 시 RuntimeException 발생 - 캡슐 삭제") + void deleteCapsule_SecurityUtilThrowsRuntimeException() { + try (MockedStatic mockedSecurityUtil = mockStatic(SecurityUtil.class)) { + mockedSecurityUtil.when(SecurityUtil::getCurrentUsername) + .thenThrow(new RuntimeException("인증되지 않은 접근입니다.")); // 메시지 수정 + + RuntimeException exception = assertThrows(RuntimeException.class, () -> + capsuleService.deleteCapsule(1L) + ); + + assertThat(exception.getMessage()).isEqualTo("인증되지 않은 접근입니다."); // 메시지 수정 + mockedSecurityUtil.verify(SecurityUtil::getCurrentUsername, times(1)); + verify(capsuleRepository, never()).findById(anyLong()); + verify(capsuleRepository, never()).delete(any(Capsule.class)); + } + } + + @Test + @DisplayName("현재 사용자 가져오기 실패 - 사용자를 찾을 수 없음 (DB에 없음)") + void getCurrentUser_UserNotFound() { + try (MockedStatic mockedSecurityUtil = mockStatic(SecurityUtil.class)) { + mockedSecurityUtil.when(SecurityUtil::getCurrentUsername) + .thenReturn("nonexistent@example.com"); // 존재하지 않는 이메일 반환 + + when(userRepository.findByEmail("nonexistent@example.com")).thenReturn(Optional.empty()); + + CustomException exception = assertThrows(CustomException.class, () -> + capsuleService.createCapsule(testRequestDto) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.USER_NOT_FOUND); + + mockedSecurityUtil.verify(SecurityUtil::getCurrentUsername, times(1)); + verify(userRepository, times(1)).findByEmail("nonexistent@example.com"); + } + } +} \ No newline at end of file