diff --git a/backend/src/main/java/com/festago/admin/dto/upload/AdminDeleteAbandonedPeriodUploadFileV1Request.java b/backend/src/main/java/com/festago/admin/dto/upload/AdminDeleteAbandonedPeriodUploadFileV1Request.java new file mode 100644 index 000000000..014e8c2f8 --- /dev/null +++ b/backend/src/main/java/com/festago/admin/dto/upload/AdminDeleteAbandonedPeriodUploadFileV1Request.java @@ -0,0 +1,11 @@ +package com.festago.admin.dto.upload; + +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; + +public record AdminDeleteAbandonedPeriodUploadFileV1Request( + @NotNull LocalDateTime startTime, + @NotNull LocalDateTime endTime +) { + +} diff --git a/backend/src/main/java/com/festago/admin/presentation/v1/AdminUploadFileDeleteV1Controller.java b/backend/src/main/java/com/festago/admin/presentation/v1/AdminUploadFileDeleteV1Controller.java new file mode 100644 index 000000000..2ca48930c --- /dev/null +++ b/backend/src/main/java/com/festago/admin/presentation/v1/AdminUploadFileDeleteV1Controller.java @@ -0,0 +1,37 @@ +package com.festago.admin.presentation.v1; + +import com.festago.admin.dto.upload.AdminDeleteAbandonedPeriodUploadFileV1Request; +import com.festago.upload.application.UploadFileDeleteService; +import io.swagger.v3.oas.annotations.Hidden; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/admin/api/v1/upload/delete") +@RequiredArgsConstructor +@Hidden +public class AdminUploadFileDeleteV1Controller { + + private final UploadFileDeleteService uploadFileDeleteService; + + @DeleteMapping("/abandoned-period") + public ResponseEntity deleteAbandonedWithPeriod( + @RequestBody @Valid AdminDeleteAbandonedPeriodUploadFileV1Request request + ) { + uploadFileDeleteService.deleteAbandonedStatusWithPeriod(request.startTime(), request.endTime()); + return ResponseEntity.ok() + .build(); + } + + @DeleteMapping("/old-uploaded") + public ResponseEntity deleteOldUploaded() { + uploadFileDeleteService.deleteOldUploadedStatus(); + return ResponseEntity.ok() + .build(); + } +} diff --git a/backend/src/main/java/com/festago/admin/presentation/v1/AdminUploadV1Controller.java b/backend/src/main/java/com/festago/admin/presentation/v1/AdminUploadImageV1Controller.java similarity index 96% rename from backend/src/main/java/com/festago/admin/presentation/v1/AdminUploadV1Controller.java rename to backend/src/main/java/com/festago/admin/presentation/v1/AdminUploadImageV1Controller.java index 9ae8b0cd2..3530aae0e 100644 --- a/backend/src/main/java/com/festago/admin/presentation/v1/AdminUploadV1Controller.java +++ b/backend/src/main/java/com/festago/admin/presentation/v1/AdminUploadImageV1Controller.java @@ -18,7 +18,7 @@ @RequestMapping("/admin/api/v1/upload/images") @RequiredArgsConstructor @Hidden -public class AdminUploadV1Controller { +public class AdminUploadImageV1Controller { private final ImageFileUploadService imageFileUploadService; diff --git a/backend/src/main/java/com/festago/upload/application/UploadFileDeleteService.java b/backend/src/main/java/com/festago/upload/application/UploadFileDeleteService.java new file mode 100644 index 000000000..05d4d6a80 --- /dev/null +++ b/backend/src/main/java/com/festago/upload/application/UploadFileDeleteService.java @@ -0,0 +1,38 @@ +package com.festago.upload.application; + +import com.festago.upload.domain.StorageClient; +import com.festago.upload.domain.UploadFile; +import com.festago.upload.domain.UploadStatus; +import com.festago.upload.repository.UploadFileRepository; +import java.time.Clock; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class UploadFileDeleteService { + + private final StorageClient storageClient; + private final UploadFileRepository uploadFileRepository; + private final Clock clock; + + public void deleteAbandonedStatusWithPeriod(LocalDateTime startTime, LocalDateTime endTime) { + List uploadFiles = uploadFileRepository.findByCreatedAtBetweenAndStatus(startTime, endTime, UploadStatus.ABANDONED); + deleteUploadFiles(uploadFiles); + } + + private void deleteUploadFiles(List uploadFiles) { + storageClient.delete(uploadFiles); + uploadFileRepository.deleteByIn(uploadFiles); + } + + public void deleteOldUploadedStatus() { + LocalDateTime yesterday = LocalDateTime.now(clock).minusDays(1); + List uploadFiles = uploadFileRepository.findByCreatedAtBeforeAndStatus(yesterday, UploadStatus.UPLOADED); + deleteUploadFiles(uploadFiles); + } +} diff --git a/backend/src/main/java/com/festago/upload/domain/StorageClient.java b/backend/src/main/java/com/festago/upload/domain/StorageClient.java index 6cbc55a8d..65914f4cd 100644 --- a/backend/src/main/java/com/festago/upload/domain/StorageClient.java +++ b/backend/src/main/java/com/festago/upload/domain/StorageClient.java @@ -1,15 +1,24 @@ package com.festago.upload.domain; +import java.util.List; import org.springframework.web.multipart.MultipartFile; public interface StorageClient { /** - * MultipartFile을 보관(영속)하는 클래스
업로드 작업이 끝나면, 업로드한 파일의 정보를 가진 UploadStatus.UPLOADED 상태의 UploadFile를 반환해야 한다. - *
반환된 UploadFile을 영속하는 책임은 해당 클래스를 사용하는 클라이언트가 구현해야 한다.
+ * MultipartFile을 보관(영속)하는 메서드
업로드 작업이 끝나면, 업로드한 파일의 정보를 가진 UploadStatus.UPLOADED 상태의 UploadFile를 반환해야 한다. + *
반환된 UploadFile을 영속하는 책임은 해당 메서드를 사용하는 클라이언트가 구현해야 한다.
* * @param file 업로드 할 MultipartFile * @return UploadStatus.PENDING 상태의 영속되지 않은 UploadFile 엔티티 */ UploadFile storage(MultipartFile file); + + /** + * 업로드 파일을 삭제하는 메서드
삭제 작업이 끝나면, UploadFile이 가진 정보에 대한 업로드 된 파일이 없으므로, 인자로 들어온 UploadFiles를 삭제해야 한다.
삭제가 + * 끝나고 UploadFile을 삭제하는 책임은 해당 메서드를 사용하는 클라이언트가 구현해야 한다.
+ * + * @param uploadFiles 삭제하려는 업로드 된 파일의 정보가 담긴 UploadFile 목록 + */ + void delete(List uploadFiles); } diff --git a/backend/src/main/java/com/festago/upload/infrastructure/R2StorageClient.java b/backend/src/main/java/com/festago/upload/infrastructure/R2StorageClient.java index ec56b29e2..e7f9b383b 100644 --- a/backend/src/main/java/com/festago/upload/infrastructure/R2StorageClient.java +++ b/backend/src/main/java/com/festago/upload/infrastructure/R2StorageClient.java @@ -10,6 +10,7 @@ import java.net.URI; import java.time.Clock; import java.time.LocalDateTime; +import java.util.List; import java.util.UUID; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -20,7 +21,11 @@ import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse; +import software.amazon.awssdk.services.s3.model.ObjectIdentifier; import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.S3Error; @Slf4j @Component @@ -71,12 +76,44 @@ private void upload(MultipartFile file, UploadFile uploadFile) { String mimeType = uploadFile.getMimeType().toString(); RequestBody requestBody = RequestBody.fromContentProvider(() -> inputStream, fileSize, mimeType); UUID uploadFileId = uploadFile.getId(); - log.info("파일 업로드 시작. id = {}, uploadUri={}, size={}", uploadFileId, uploadFile.getUploadUri(), fileSize); + log.info("파일 업로드 시작. id={}, uploadUri={}, size={}", uploadFileId, uploadFile.getUploadUri(), fileSize); s3Client.putObject(objectRequest, requestBody); - log.info("파일 업로드 완료. id = {}", uploadFileId); + log.info("파일 업로드 완료. id={}", uploadFileId); } catch (IOException e) { - log.warn("파일 업로드 중 문제가 발생했습니다. id = {}", uploadFile.getId()); + log.warn("파일 업로드 중 문제가 발생했습니다. id={}", uploadFile.getId()); throw new InternalServerException(ErrorCode.FILE_UPLOAD_ERROR, e); } } + + @Override + public void delete(List uploadFiles) { + if (uploadFiles.isEmpty()) { + log.info("삭제하려는 파일이 없습니다."); + return; + } + int fileSize = uploadFiles.size(); + UUID firstFileId = uploadFiles.get(0).getId(); + DeleteObjectsRequest deleteObjectsRequest = getDeleteObjectsRequest(uploadFiles); + + log.info("{}개 파일 삭제 시작. 첫 번째 파일 식별자={}", fileSize, firstFileId); + DeleteObjectsResponse response = s3Client.deleteObjects(deleteObjectsRequest); + log.info("{}개 파일 삭제 완료. 첫 번째 파일 식별자={}", fileSize, firstFileId); + + if (response.hasErrors()) { + List errors = response.errors(); + log.warn("{}개 파일 삭제 중 에러가 발생했습니다. 첫 번째 파일 식별자={}, 에러 개수={}", fileSize, firstFileId, errors.size()); + errors.forEach(error -> log.info("파일 삭제 중 에러가 발생했습니다. key={}, message={}", error.key(), error.message())); + } + } + + private DeleteObjectsRequest getDeleteObjectsRequest(List uploadFiles) { + List objectIdentifiers = uploadFiles.stream() + .map(UploadFile::getName) + .map(name -> ObjectIdentifier.builder().key(name).build()) + .toList(); + return DeleteObjectsRequest.builder() + .bucket(bucket) + .delete(builder -> builder.objects(objectIdentifiers).build()) + .build(); + } } diff --git a/backend/src/main/java/com/festago/upload/repository/UploadFileRepository.java b/backend/src/main/java/com/festago/upload/repository/UploadFileRepository.java index 5029aae61..b2eb159ab 100644 --- a/backend/src/main/java/com/festago/upload/repository/UploadFileRepository.java +++ b/backend/src/main/java/com/festago/upload/repository/UploadFileRepository.java @@ -2,11 +2,16 @@ import com.festago.upload.domain.FileOwnerType; import com.festago.upload.domain.UploadFile; +import com.festago.upload.domain.UploadStatus; +import java.time.LocalDateTime; import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.UUID; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; public interface UploadFileRepository extends Repository { @@ -17,4 +22,13 @@ public interface UploadFileRepository extends Repository { List findAllByOwnerIdAndOwnerType(Long ownerId, FileOwnerType ownerType); List findByIdIn(Collection ids); + + List findByCreatedAtBetweenAndStatus(LocalDateTime startTime, LocalDateTime endTime, + UploadStatus status); + + List findByCreatedAtBeforeAndStatus(LocalDateTime createdAt, UploadStatus status); + + @Modifying + @Query("delete from UploadFile uf where uf in :uploadFiles") + void deleteByIn(@Param("uploadFiles") List uploadFiles); } diff --git a/backend/src/test/java/com/festago/admin/presentation/v1/AdminUploadFileDeleteV1ControllerTest.java b/backend/src/test/java/com/festago/admin/presentation/v1/AdminUploadFileDeleteV1ControllerTest.java new file mode 100644 index 000000000..e92023eb8 --- /dev/null +++ b/backend/src/test/java/com/festago/admin/presentation/v1/AdminUploadFileDeleteV1ControllerTest.java @@ -0,0 +1,114 @@ +package com.festago.admin.presentation.v1; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.festago.admin.dto.upload.AdminDeleteAbandonedPeriodUploadFileV1Request; +import com.festago.auth.domain.Role; +import com.festago.support.CustomWebMvcTest; +import com.festago.support.WithMockAuth; +import jakarta.servlet.http.Cookie; +import java.time.LocalDateTime; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +@CustomWebMvcTest +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AdminUploadFileDeleteV1ControllerTest { + + private static final Cookie TOKEN_COOKIE = new Cookie("token", "token"); + + @Autowired + MockMvc mockMvc; + + @Autowired + ObjectMapper objectMapper; + + @Nested + class ABANDONED_상태와_기간에_포함되는_파일_삭제 { + + final String uri = "/admin/api/v1/upload/delete/abandoned-period"; + + @Nested + @DisplayName("DELETE " + uri) + class 올바른_주소로 { + + @Test + @WithMockAuth(role = Role.ADMIN) + void 요청을_보내면_200_응답이_반환된다() throws Exception { + // given + LocalDateTime now = LocalDateTime.now(); + var request = new AdminDeleteAbandonedPeriodUploadFileV1Request(now, now); + + // when & then + mockMvc.perform(delete(uri) + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON) + .cookie(TOKEN_COOKIE)) + .andExpect(status().isOk()); + } + + @Test + void 토큰_없이_보내면_401_응답이_반환된다() throws Exception { + // when & then + mockMvc.perform(delete(uri)) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithMockAuth(role = Role.MEMBER) + void 토큰의_권한이_Admin이_아니면_404_응답이_반환된다() throws Exception { + // when & then + mockMvc.perform(delete(uri) + .cookie(TOKEN_COOKIE)) + .andExpect(status().isNotFound()); + } + } + } + + @Nested + class 오래된_UPLOADED_상태_파일_삭제 { + + final String uri = "/admin/api/v1/upload/delete/old-uploaded"; + + @Nested + @DisplayName("DELETE " + uri) + class 올바른_주소로 { + + @Test + @WithMockAuth(role = Role.ADMIN) + void 요청을_보내면_200_응답이_반환된다() throws Exception { + // given + // when & then + mockMvc.perform(delete(uri) + .contentType(MediaType.APPLICATION_JSON) + .cookie(TOKEN_COOKIE)) + .andExpect(status().isOk()); + } + + @Test + void 토큰_없이_보내면_401_응답이_반환된다() throws Exception { + // when & then + mockMvc.perform(delete(uri)) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithMockAuth(role = Role.MEMBER) + void 토큰의_권한이_Admin이_아니면_404_응답이_반환된다() throws Exception { + // when & then + mockMvc.perform(delete(uri) + .cookie(TOKEN_COOKIE)) + .andExpect(status().isNotFound()); + } + } + } +} diff --git a/backend/src/test/java/com/festago/admin/presentation/v1/AdminUploadV1ControllerTest.java b/backend/src/test/java/com/festago/admin/presentation/v1/AdminUploadImageV1ControllerTest.java similarity index 98% rename from backend/src/test/java/com/festago/admin/presentation/v1/AdminUploadV1ControllerTest.java rename to backend/src/test/java/com/festago/admin/presentation/v1/AdminUploadImageV1ControllerTest.java index 5476791e2..a230a1d26 100644 --- a/backend/src/test/java/com/festago/admin/presentation/v1/AdminUploadV1ControllerTest.java +++ b/backend/src/test/java/com/festago/admin/presentation/v1/AdminUploadImageV1ControllerTest.java @@ -28,7 +28,7 @@ @CustomWebMvcTest @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class AdminUploadV1ControllerTest { +class AdminUploadImageV1ControllerTest { private static final Cookie TOKEN_COOKIE = new Cookie("token", "token"); diff --git a/backend/src/test/java/com/festago/upload/application/UploadFileDeleteServiceTest.java b/backend/src/test/java/com/festago/upload/application/UploadFileDeleteServiceTest.java new file mode 100644 index 000000000..252a30880 --- /dev/null +++ b/backend/src/test/java/com/festago/upload/application/UploadFileDeleteServiceTest.java @@ -0,0 +1,147 @@ +package com.festago.upload.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.spy; + +import com.festago.support.TimeInstantProvider; +import com.festago.support.fixture.UploadFileFixture; +import com.festago.upload.domain.UploadFile; +import com.festago.upload.infrastructure.FakeStorageClient; +import com.festago.upload.repository.MemoryUploadFileRepository; +import com.festago.upload.repository.UploadFileRepository; +import java.time.Clock; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class UploadFileDeleteServiceTest { + + UploadFileDeleteService uploadFileDeleteService; + + UploadFileRepository uploadFileRepository; + + Clock clock; + + @BeforeEach + void setUp() { + uploadFileRepository = new MemoryUploadFileRepository(); + clock = spy(Clock.systemDefaultZone()); + uploadFileDeleteService = new UploadFileDeleteService( + new FakeStorageClient(), + uploadFileRepository, + clock + ); + } + + @Nested + class deleteAbandonedStatusWithPeriod { + + LocalDateTime _6월_30일_18시_0분_0초 = LocalDateTime.parse("2077-06-30T18:00:00"); + LocalDateTime _6월_30일_18시_0분_1초 = LocalDateTime.parse("2077-06-30T18:00:01"); + LocalDateTime _6월_30일_18시_0분_2초 = LocalDateTime.parse("2077-06-30T18:00:02"); + + @Test + void 삭제되는_파일은_시작일에_포함되고_종료일에도_포함된다() { + // given + UploadFile _6월_30일_18시_0분_0초_생성된_파일 = uploadFileRepository.save( + UploadFileFixture.builder().createdAt(_6월_30일_18시_0분_0초).buildAbandoned()); + UploadFile _6월_30일_18시_0분_1초_생성된_파일 = uploadFileRepository.save( + UploadFileFixture.builder().createdAt(_6월_30일_18시_0분_1초).buildAbandoned()); + UploadFile _6월_30일_18시_0분_2초_생성된_파일 = uploadFileRepository.save( + UploadFileFixture.builder().createdAt(_6월_30일_18시_0분_2초).buildAbandoned()); + + // when + uploadFileDeleteService.deleteAbandonedStatusWithPeriod(_6월_30일_18시_0분_0초, _6월_30일_18시_0분_1초); + + // then + var expect = uploadFileRepository.findByIdIn(List.of( + _6월_30일_18시_0분_0초_생성된_파일.getId(), + _6월_30일_18시_0분_1초_생성된_파일.getId(), + _6월_30일_18시_0분_2초_생성된_파일.getId() + )); + assertThat(expect) + .map(UploadFile::getId) + .containsExactly(_6월_30일_18시_0분_2초_생성된_파일.getId()); + } + + @Test + void ABANDONED_상태의_파일만_삭제된다() { + // given + UploadFile UPLOADED_상태_파일 = uploadFileRepository.save( + UploadFileFixture.builder().createdAt(_6월_30일_18시_0분_0초).build()); + UploadFile ABANDONED_상태_파일 = uploadFileRepository.save( + UploadFileFixture.builder().createdAt(_6월_30일_18시_0분_0초).buildAbandoned()); + + // when + uploadFileDeleteService.deleteAbandonedStatusWithPeriod(_6월_30일_18시_0분_0초, _6월_30일_18시_0분_0초); + + // then + assertThat(uploadFileRepository.findById(UPLOADED_상태_파일.getId())).isPresent(); + assertThat(uploadFileRepository.findById(ABANDONED_상태_파일.getId())).isEmpty(); + } + } + + @Nested + class deleteOldUploadedStatus { + + LocalDateTime _6월_29일_17시_59분_59초 = LocalDateTime.parse("2077-06-29T17:59:59"); + LocalDateTime _6월_29일_18시_0분_0초 = LocalDateTime.parse("2077-06-29T18:00:00"); + LocalDateTime _6월_29일_18시_0분_1초 = LocalDateTime.parse("2077-06-29T18:00:01"); + LocalDateTime _6월_30일_18시_0분_1초 = LocalDateTime.parse("2077-06-30T18:00:01"); + + @Test + void 생성된지_정확히_하루가_지난_파일만_삭제된다() { + // given + UploadFile _6월_29일_17시_59분_59초_생성된_파일 = uploadFileRepository.save( + UploadFileFixture.builder().createdAt(_6월_29일_17시_59분_59초).build()); + UploadFile _6월_29일_18시_0분_0초_생성된_파일 = uploadFileRepository.save( + UploadFileFixture.builder().createdAt(_6월_29일_18시_0분_0초).build()); + UploadFile _6월_29일_18시_0분_1초_생성된_파일 = uploadFileRepository.save( + UploadFileFixture.builder().createdAt(_6월_29일_18시_0분_1초).build()); + + LocalDateTime now = _6월_30일_18시_0분_1초; + given(clock.instant()) + .willReturn(TimeInstantProvider.from(now)); + + // when + uploadFileDeleteService.deleteOldUploadedStatus(); + + // then + var expect = uploadFileRepository.findByIdIn(List.of( + _6월_29일_17시_59분_59초_생성된_파일.getId(), + _6월_29일_18시_0분_0초_생성된_파일.getId(), + _6월_29일_18시_0분_1초_생성된_파일.getId() + )); + assertThat(expect) + .map(UploadFile::getId) + .containsExactly(_6월_29일_18시_0분_1초_생성된_파일.getId()); + } + + @Test + void UPLOADED_상태의_파일만_삭제된다() { + // given + UploadFile UPLOADED_상태_파일 = uploadFileRepository.save( + UploadFileFixture.builder().createdAt(_6월_29일_18시_0분_0초).build()); + UploadFile ABANDONED_상태_파일 = uploadFileRepository.save( + UploadFileFixture.builder().createdAt(_6월_29일_18시_0분_0초).buildAbandoned()); + + LocalDateTime now = _6월_30일_18시_0분_1초; + given(clock.instant()) + .willReturn(TimeInstantProvider.from(now)); + + // when + uploadFileDeleteService.deleteOldUploadedStatus(); + + // then + assertThat(uploadFileRepository.findById(UPLOADED_상태_파일.getId())).isEmpty(); + assertThat(uploadFileRepository.findById(ABANDONED_상태_파일.getId())).isPresent(); + } + } +} diff --git a/backend/src/test/java/com/festago/upload/infrastructure/FakeStorageClient.java b/backend/src/test/java/com/festago/upload/infrastructure/FakeStorageClient.java index dcc0ab55c..e57533708 100644 --- a/backend/src/test/java/com/festago/upload/infrastructure/FakeStorageClient.java +++ b/backend/src/test/java/com/festago/upload/infrastructure/FakeStorageClient.java @@ -3,6 +3,7 @@ import com.festago.support.fixture.UploadFileFixture; import com.festago.upload.domain.StorageClient; import com.festago.upload.domain.UploadFile; +import java.util.List; import org.springframework.web.multipart.MultipartFile; public class FakeStorageClient implements StorageClient { @@ -11,4 +12,9 @@ public class FakeStorageClient implements StorageClient { public UploadFile storage(MultipartFile file) { return UploadFileFixture.builder().build(); } + + @Override + public void delete(List uploadFiles) { + // NOOP + } } diff --git a/backend/src/test/java/com/festago/upload/repository/MemoryUploadFileRepository.java b/backend/src/test/java/com/festago/upload/repository/MemoryUploadFileRepository.java index f7a141061..82a3c8462 100644 --- a/backend/src/test/java/com/festago/upload/repository/MemoryUploadFileRepository.java +++ b/backend/src/test/java/com/festago/upload/repository/MemoryUploadFileRepository.java @@ -2,6 +2,8 @@ import com.festago.upload.domain.FileOwnerType; import com.festago.upload.domain.UploadFile; +import com.festago.upload.domain.UploadStatus; +import java.time.LocalDateTime; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -38,4 +40,29 @@ public List findByIdIn(Collection ids) { .filter(uploadFile -> ids.contains(uploadFile.getId())) .toList(); } + + @Override + public List findByCreatedAtBetweenAndStatus(LocalDateTime startTime, LocalDateTime endTime, + UploadStatus status) { + return memory.values().stream() + .filter(it -> it.getStatus() == status) + .filter(it -> it.getCreatedAt().isEqual(startTime) || it.getCreatedAt().isAfter(startTime)) + .filter(it -> it.getCreatedAt().isEqual(endTime) || it.getCreatedAt().isBefore(endTime)) + .toList(); + } + + @Override + public List findByCreatedAtBeforeAndStatus(LocalDateTime createdAt, UploadStatus status) { + return memory.values().stream() + .filter(it -> it.getStatus() == status) + .filter(it -> it.getCreatedAt().isBefore(createdAt)) + .toList(); + } + + @Override + public void deleteByIn(List uploadFiles) { + for (UploadFile uploadFile : uploadFiles) { + memory.remove(uploadFile.getId()); + } + } }