diff --git a/build.gradle b/build.gradle index 9229ad2..af89980 100644 --- a/build.gradle +++ b/build.gradle @@ -32,11 +32,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-actuator' - // TestContainer - testImplementation "org.testcontainers:testcontainers:1.19.3" - testImplementation "org.testcontainers:junit-jupiter:1.19.3" - testImplementation "org.testcontainers:mysql:1.19.3" - // Micrometer implementation 'io.micrometer:micrometer-registry-prometheus' // Prometheus @@ -66,6 +61,7 @@ dependencies { implementation 'com.google.firebase:firebase-admin:9.2.0' // test containers + testImplementation "org.testcontainers:testcontainers" testImplementation 'org.testcontainers:mysql' testImplementation 'org.testcontainers:jdbc' testImplementation 'org.testcontainers:junit-jupiter' @@ -112,11 +108,11 @@ tasks.register('copyDocument', Copy) { // 8 dependsOn asciidoctor from file("build/docs/asciidoc") - into file("src/main/resources/static/docs") + into file("docs") } asciidoctor.doFirst { - delete file('src/main/resources/static/docs') + delete file('docs') } diff --git a/src/main/java/com/todaysound/todaysound_server/TodaysoundServerApplication.java b/src/main/java/com/todaysound/todaysound_server/TodaysoundServerApplication.java index 4dceb27..d65e9f7 100644 --- a/src/main/java/com/todaysound/todaysound_server/TodaysoundServerApplication.java +++ b/src/main/java/com/todaysound/todaysound_server/TodaysoundServerApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableScheduling public class TodaysoundServerApplication { public static void main(String[] args) { diff --git a/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/InternalAlertController.java b/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/InternalAlertController.java index c84207e..051ca98 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/InternalAlertController.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/InternalAlertController.java @@ -43,13 +43,7 @@ public void createAlert(@RequestBody InternalAlertRequest request) { // 알림이 활성화된 구독에 대해서만 푸시 전송 if (subscription.isAlarmEnabled()) { User user = subscription.getUser(); - String prefix; - - if (request.keywordMatched == true) { - prefix = "[" + request.siteAlias + "]"; - } else { - prefix = "[긴급/" + request.siteAlias + "]"; - } + String prefix = "[" + request.siteAlias + "]"; fcmService.sendNotificationToUser( user, diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/Keyword.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/Keyword.java index 026ea1c..2aaac26 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/Keyword.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/Keyword.java @@ -16,16 +16,13 @@ @Entity @Getter -@Builder @Table(name = "keywords") @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) public class Keyword extends BaseEntity { @Column(name = "name", nullable = false) private String name; - @Builder.Default @OneToMany(mappedBy = "keyword", cascade = CascadeType.ALL, orphanRemoval = true) private List subscriptions = new ArrayList<>(); diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/Subscription.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/Subscription.java index 52a9dda..8b1584f 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/Subscription.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/Subscription.java @@ -16,7 +16,6 @@ import java.util.ArrayList; import java.util.List; import lombok.AccessLevel; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -27,10 +26,8 @@ @Entity @Getter -@Builder @Table(name = "subscriptions") @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) public class Subscription extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @@ -44,9 +41,8 @@ public class Subscription extends BaseEntity { private boolean isAlarmEnabled; // 마지막으로 처리한 게시물의 site_post_id - @Builder.Default @Column(name = "last_seen_post_id", nullable = false) - private String lastSeenPostId = ""; + private String lastSeenPostId; @CreationTimestamp @Column(name = "created_at", nullable = false, updatable = false) @@ -60,12 +56,10 @@ public class Subscription extends BaseEntity { @JoinColumn(name = "url_id", nullable = false) private Url url; - @Builder.Default @OneToMany(mappedBy = "subscription", cascade = CascadeType.ALL, orphanRemoval = true) @OnDelete(action = OnDeleteAction.CASCADE) private List subscriptionKeywords = new ArrayList<>(); - @Builder.Default @OneToMany(mappedBy = "subscription", cascade = CascadeType.ALL, orphanRemoval = true) @OnDelete(action = OnDeleteAction.CASCADE) private List summaries = new ArrayList<>(); @@ -100,4 +94,18 @@ public void updateKeywords(List keywords) { keywords.forEach(keyword -> this.subscriptionKeywords.add(SubscriptionKeyword.of(this, keyword))); } + + @Builder + private Subscription(Url url, boolean isAlarmEnabled, String alias, User user, String lastSeenPostId) { + this.url = url; + this.isAlarmEnabled = isAlarmEnabled; + this.alias = alias; + this.user = user; + this.lastSeenPostId = lastSeenPostId; + } + + public static Subscription create(Url url, boolean isAlarmEnabled, String alias, User user, String lastSeenPostId) { + return Subscription.builder().url(url).isAlarmEnabled(isAlarmEnabled).alias(alias).user(user) + .lastSeenPostId(lastSeenPostId).build(); + } } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/repository/SubscriptionDynamicRepository.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/repository/SubscriptionDynamicRepository.java index adf208d..39ba55b 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/repository/SubscriptionDynamicRepository.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/repository/SubscriptionDynamicRepository.java @@ -4,5 +4,5 @@ import java.util.List; public interface SubscriptionDynamicRepository { - List findByUserId(Long userId, Long cursor, Integer size); + List findByUserId(Long userId, Integer cursor, Integer size); } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/repository/SubscriptionDynamicRepositoryImpl.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/repository/SubscriptionDynamicRepositoryImpl.java index 2cc4a8c..14099ce 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/repository/SubscriptionDynamicRepositoryImpl.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/repository/SubscriptionDynamicRepositoryImpl.java @@ -17,7 +17,7 @@ public class SubscriptionDynamicRepositoryImpl implements SubscriptionDynamicRep private final JPAQueryFactory queryFactory; @Override - public List findByUserId(Long userId, Long page, Integer size) { + public List findByUserId(Long userId, Integer page, Integer size) { return queryFactory .selectFrom(subscription) diff --git a/src/main/java/com/todaysound/todaysound_server/domain/summary/entity/Summary.java b/src/main/java/com/todaysound/todaysound_server/domain/summary/entity/Summary.java index 0d81086..51cd904 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/summary/entity/Summary.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/summary/entity/Summary.java @@ -64,10 +64,4 @@ public static Summary create(String hash, String title, String content, return summary; } - // Summary를 읽음 처리 - public void markAsRead() { - this.isKeywordMatched = true; - this.updatedAt = LocalDateTime.now(); - } - } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/summary/infra/scheduler/SummaryCleanupScheduler.java b/src/main/java/com/todaysound/todaysound_server/domain/summary/infra/scheduler/SummaryCleanupScheduler.java new file mode 100644 index 0000000..9dd2d89 --- /dev/null +++ b/src/main/java/com/todaysound/todaysound_server/domain/summary/infra/scheduler/SummaryCleanupScheduler.java @@ -0,0 +1,24 @@ +package com.todaysound.todaysound_server.domain.summary.infra.scheduler; + +import com.todaysound.todaysound_server.domain.summary.repository.SummaryRepository; +import java.time.LocalDateTime; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +public class SummaryCleanupScheduler { + + private final SummaryRepository summaryRepository; + + @Transactional + @Scheduled(cron = "0 0 3 * * *") // 매일 새벽 3시에 실행 + public void deleteOldSummaries() { + + LocalDateTime threshold = LocalDateTime.now().minusDays(7); + summaryRepository.deleteByCreatedAtBefore(threshold); + + } +} diff --git a/src/main/java/com/todaysound/todaysound_server/domain/summary/repository/SummaryRepository.java b/src/main/java/com/todaysound/todaysound_server/domain/summary/repository/SummaryRepository.java index df768cb..d6a1b43 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/summary/repository/SummaryRepository.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/summary/repository/SummaryRepository.java @@ -1,6 +1,7 @@ package com.todaysound.todaysound_server.domain.summary.repository; import com.todaysound.todaysound_server.domain.summary.entity.Summary; +import java.time.LocalDateTime; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -13,6 +14,10 @@ public interface SummaryRepository extends JpaRepository { */ Optional findById(Long id); + /** + * 생성일 기준으로 오래된 Summary 삭제 + */ + void deleteByCreatedAtBefore(LocalDateTime dateTime); } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/url/entity/Url.java b/src/main/java/com/todaysound/todaysound_server/domain/url/entity/Url.java index 575a9fb..9adac6e 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/url/entity/Url.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/url/entity/Url.java @@ -5,6 +5,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.Table; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -20,4 +21,13 @@ public class Url extends BaseEntity { @Column String title; + @Builder + private Url(String link, String title) { + this.link = link; + this.title = title; + } + + public static Url create(String link, String title) { + return Url.builder().link(link).title(title).build(); + } } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/user/entity/FCM_Token.java b/src/main/java/com/todaysound/todaysound_server/domain/user/entity/FCM_Token.java index a9b993c..dfb4218 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/user/entity/FCM_Token.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/user/entity/FCM_Token.java @@ -7,17 +7,14 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import lombok.AccessLevel; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter -@Builder @Table(name = "fcm_tokens") @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor public class FCM_Token extends BaseEntity { @ManyToOne @@ -33,4 +30,15 @@ public class FCM_Token extends BaseEntity { public void update(String sFcmToken) { this.fcmToken = sFcmToken; } + + @Builder + private FCM_Token(User user, String fcmToken, String model) { + this.user = user; + this.fcmToken = fcmToken; + this.model = model; + } + + public static FCM_Token create(User user, String fcmToken, String model) { + return FCM_Token.builder().user(user).fcmToken(fcmToken).model(model).build(); + } } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/user/entity/User.java b/src/main/java/com/todaysound/todaysound_server/domain/user/entity/User.java index 6dc958d..d9a508d 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/user/entity/User.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/user/entity/User.java @@ -23,10 +23,8 @@ @Entity @Getter @Setter -@Builder @Table(name = "users") @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) public class User extends BaseEntity { // ********************************* static final 상수 필드 *********************************/ @@ -69,7 +67,6 @@ public class User extends BaseEntity { /** * 사용자 활성 상태 기본값 true, 탈퇴 시 false로 변경 */ - @Builder.Default @Column(name = "is_active", nullable = false) private boolean isActive = true; @@ -92,12 +89,9 @@ public class User extends BaseEntity { * cascade = CascadeType.ALL: User 삭제 시 관련 Subscription도 함께 삭제 orphanRemoval = true: 고아 객체(연관관계가 끊어진 객체) 자동 삭제 fetch * = FetchType.LAZY: 지연 로딩으로 성능 최적화 */ - @Builder.Default - @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, - fetch = FetchType.LAZY) + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) private List subscriptions = new ArrayList<>(); - @Builder.Default @OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.ALL) private List fcmTokenList = new ArrayList<>(); @@ -162,4 +156,40 @@ public void addFcmToken(FCM_Token fcmToken) { // FCM_Token의 user 필드는 builder에서 이미 설정되어야 함 this.fcmTokenList.add(fcmToken); } + + @Builder + private User(String userId, String hashedSecret, String secretFingerprint, UserType userType, boolean isActive, + String plainSecret, List fcmTokenList) { + this.userId = userId; + this.hashedSecret = hashedSecret; + this.secretFingerprint = secretFingerprint; + this.userType = userType; + this.isActive = isActive; + this.plainSecret = plainSecret; + this.fcmTokenList = fcmTokenList; + } + + public static User createAnonymous(String userId, String hashedSecret, String secretFingerprint, UserType userType, + boolean isActive, String plainSecret, List fcmTokenList) { + return User.builder().userId(userId) + .hashedSecret(hashedSecret) + .secretFingerprint(secretFingerprint) + .userType(userType) + .isActive(isActive) + .plainSecret(plainSecret) + .fcmTokenList(fcmTokenList) + .build(); + } + + public static User create(String userId, String hashedSecret, String secretFingerprint, UserType userType, + boolean isActive, String plainSecret) { + return User.builder().userId(userId) + .hashedSecret(hashedSecret) + .secretFingerprint(secretFingerprint) + .userType(userType) + .isActive(isActive) + .plainSecret(plainSecret) + .fcmTokenList(new ArrayList<>()) + .build(); + } } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/user/factory/UserFactory.java b/src/main/java/com/todaysound/todaysound_server/domain/user/factory/UserFactory.java index b3bafc2..e5bfe4c 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/user/factory/UserFactory.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/user/factory/UserFactory.java @@ -23,8 +23,7 @@ public class UserFactory { public User createAnonymousUser(UserSecretRequest userSecretRequest) { // 배포시 로깅은 제거 - log.debug("익명 사용자 생성 시작: deviceSecret={}", - userSecretRequest.deviceSecret().substring(0, 8) + "..."); + log.debug("익명 사용자 생성 시작: deviceSecret={}", userSecretRequest.deviceSecret().substring(0, 8) + "..."); // UUID 생성 String userId = UUID.randomUUID().toString(); @@ -35,12 +34,8 @@ public User createAnonymousUser(UserSecretRequest userSecretRequest) { // 중복 검사용 fingerprint 생성 (SHA-256) String secretFingerprint = CryptoUtils.sha256(userSecretRequest.deviceSecret()); - // User 엔티티 생성 - User user = User.builder().userId(userId).hashedSecret(hashedSecret) - .secretFingerprint(secretFingerprint).userType(UserType.ANONYMOUS).isActive(true) - .plainSecret(userSecretRequest.deviceSecret()) // 생성 시에만 설정 - .fcmTokenList(new ArrayList<>()) // 빌더 사용 시 명시적으로 초기화 - .build(); + User user = User.createAnonymous(userId, hashedSecret, secretFingerprint, UserType.ANONYMOUS, true, + userSecretRequest.deviceSecret(), new ArrayList<>()); log.debug("익명 사용자 생성 완료: userId={}", userId); diff --git a/src/main/java/com/todaysound/todaysound_server/domain/user/service/UserService.java b/src/main/java/com/todaysound/todaysound_server/domain/user/service/UserService.java index 4b8f7fa..a051156 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/user/service/UserService.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/user/service/UserService.java @@ -27,8 +27,7 @@ public class UserService { public UserIdResponse anonymous(UserSecretRequest userSecretRequest) { log.info("Anonymous user command received"); - boolean secretExists = - userQueryService.existsBySecretFingerprint(userSecretRequest.deviceSecret()); + boolean secretExists = userQueryService.existsBySecretFingerprint(userSecretRequest.deviceSecret()); User user; @@ -38,8 +37,7 @@ public UserIdResponse anonymous(UserSecretRequest userSecretRequest) { User newUser = userFactory.createAnonymousUser(userSecretRequest); - FCM_Token fcmToken = FCM_Token.builder().fcmToken(userSecretRequest.fcmToken()) - .model(userSecretRequest.model()).user(newUser).build(); + FCM_Token fcmToken = FCM_Token.create(newUser, userSecretRequest.fcmToken(), userSecretRequest.model()); newUser.addFcmToken(fcmToken); diff --git a/src/main/java/com/todaysound/todaysound_server/global/dto/PageRequest.java b/src/main/java/com/todaysound/todaysound_server/global/dto/PageRequest.java index 3051004..f613392 100644 --- a/src/main/java/com/todaysound/todaysound_server/global/dto/PageRequest.java +++ b/src/main/java/com/todaysound/todaysound_server/global/dto/PageRequest.java @@ -1,6 +1,6 @@ package com.todaysound.todaysound_server.global.dto; -public record PageRequest(Long page, Integer size) { +public record PageRequest(Integer page, Integer size) { private static final Integer DEFAULT_SIZE = 5; @Override diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..3a845c0 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,27 @@ +spring: + profiles: + active: local + jpa: + open-in-view: false + hibernate: + ddl-auto: none + flyway: + enabled: true + baseline-on-migrate: true + +server: + port: 8080 + +--- +spring: + config: + activate: + on-profile: local + import: application-local.yml + +--- +spring: + config: + activate: + on-profile: prod + import: application-prod.yml \ No newline at end of file diff --git a/src/main/resources/db/migration/V1__init.sql b/src/main/resources/db/migration/V1__init.sql index 4aba430..0bd0027 100644 --- a/src/main/resources/db/migration/V1__init.sql +++ b/src/main/resources/db/migration/V1__init.sql @@ -77,7 +77,7 @@ create table summaries content varchar(255) not null, created_at datetime(6) not null, hash_tag varchar(255) not null, - is_read bit d not null, + is_read bit not null, post_date varchar(255) null, post_url varchar(255) not null, title varchar(255) not null, diff --git a/src/test/java/com/todaysound/todaysound_server/TodaysoundServerApplicationTests.java b/src/test/java/com/todaysound/todaysound_server/TodaysoundServerApplicationTests.java deleted file mode 100644 index a3101ac..0000000 --- a/src/test/java/com/todaysound/todaysound_server/TodaysoundServerApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.todaysound.todaysound_server; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class TodaysoundServerApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/com/todaysound/todaysound_server/command/summary/infra/SummaryCleanupSchedulerTest.java b/src/test/java/com/todaysound/todaysound_server/command/summary/infra/SummaryCleanupSchedulerTest.java new file mode 100644 index 0000000..2773ea3 --- /dev/null +++ b/src/test/java/com/todaysound/todaysound_server/command/summary/infra/SummaryCleanupSchedulerTest.java @@ -0,0 +1,68 @@ +package com.todaysound.todaysound_server.command.summary.infra; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.todaysound.todaysound_server.domain.subscription.entity.Subscription; +import com.todaysound.todaysound_server.domain.subscription.repository.SubscriptionRepository; +import com.todaysound.todaysound_server.domain.summary.entity.Summary; +import com.todaysound.todaysound_server.domain.summary.infra.scheduler.SummaryCleanupScheduler; +import com.todaysound.todaysound_server.domain.summary.repository.SummaryRepository; +import com.todaysound.todaysound_server.domain.url.entity.Url; +import com.todaysound.todaysound_server.domain.url.repository.UrlRepository; +import com.todaysound.todaysound_server.domain.user.entity.User; +import com.todaysound.todaysound_server.domain.user.entity.UserType; +import com.todaysound.todaysound_server.domain.user.repository.UserRepository; +import com.todaysound.todaysound_server.support.ServiceTestSupport; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.util.ReflectionTestUtils; + +class SummaryCleanupSchedulerTest extends ServiceTestSupport { + + @Autowired + private UrlRepository urlRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private SubscriptionRepository subscriptionRepository; + + @Autowired + private SummaryRepository summaryRepository; + + @Autowired + private SummaryCleanupScheduler summaryCleanupScheduler; + + @Test + void _7일_지난_요약은_삭제한다() { + // given + Url url = Url.create("http://example.com", "Example Site"); + urlRepository.save(url); + + User user = User.create("userId1", "hashedSecret", "fingerPrint", UserType.USER, true, "plainSecret"); + userRepository.save(user); + + Subscription subscription = Subscription.create(url, true, "alias", user, "lastSeenPostId"); + subscriptionRepository.save(subscription); + + Summary oldSummary = Summary.create("hash", "title", "content", "postUrl", "postDate", true, subscription); + Summary newSummary = Summary.create("hash2", "title2", "content2", "postUrl2", "postDate2", false, subscription); + + ReflectionTestUtils.setField(oldSummary, "createdAt", LocalDateTime.now().minusDays(8)); + ReflectionTestUtils.setField(newSummary, "createdAt", LocalDateTime.now().minusDays(6)); + summaryRepository.saveAll(List.of(oldSummary, newSummary)); + + // when + summaryCleanupScheduler.deleteOldSummaries(); + + // then + List remaining = summaryRepository.findAll(); + + assertThat(remaining).hasSize(1); + assertThat(remaining.get(0).getTitle()).isEqualTo("title2"); + } + +} \ No newline at end of file diff --git a/src/test/java/com/todaysound/todaysound_server/command/summary/presentation/SummaryControllerTest.java b/src/test/java/com/todaysound/todaysound_server/command/summary/presentation/SummaryControllerTest.java index f319133..b58c615 100644 --- a/src/test/java/com/todaysound/todaysound_server/command/summary/presentation/SummaryControllerTest.java +++ b/src/test/java/com/todaysound/todaysound_server/command/summary/presentation/SummaryControllerTest.java @@ -26,14 +26,10 @@ class SummaryControllerTest extends DocumentationTestSupport { // when then mockMvc.perform(RestDocumentationRequestBuilders.delete("/api/summaries/{summaryId}", summaryId) - .header("X-User-ID", "test-user-uuid") - .header("X-Device-Secret", "test-device-secret")) - .andDo(print()) - .andExpect(status().isOk()) - .andDo(restDocsHandler.document( + .header("X-User-ID", "test-user-uuid").header("X-Device-Secret", "test-device-secret")).andDo(print()) + .andExpect(status().isOk()).andDo(restDocsHandler.document( pathParameters(parameterWithName("summaryId").description("삭제할 요약의 ID")), - responseFields( - fieldWithPath("errorCode").type(JsonFieldType.NULL).description("에러 코드, 성공 시 null"), + responseFields(fieldWithPath("errorCode").type(JsonFieldType.NULL).description("에러 코드, 성공 시 null"), fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"), fieldWithPath("result").type(JsonFieldType.NULL).description("응답 데이터, 삭제 시 null")))); } diff --git a/src/test/java/com/todaysound/todaysound_server/query/alarm/application/AlarmQueryServiceTest.java b/src/test/java/com/todaysound/todaysound_server/query/alarm/application/AlarmQueryServiceTest.java new file mode 100644 index 0000000..ca43579 --- /dev/null +++ b/src/test/java/com/todaysound/todaysound_server/query/alarm/application/AlarmQueryServiceTest.java @@ -0,0 +1,74 @@ +package com.todaysound.todaysound_server.query.alarm.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import com.todaysound.todaysound_server.domain.alarm.dto.response.RecentAlarmResponse; +import com.todaysound.todaysound_server.domain.alarm.service.AlarmQueryService; +import com.todaysound.todaysound_server.domain.subscription.entity.Subscription; +import com.todaysound.todaysound_server.domain.subscription.repository.SubscriptionRepository; +import com.todaysound.todaysound_server.domain.summary.entity.Summary; +import com.todaysound.todaysound_server.domain.summary.repository.SummaryRepository; +import com.todaysound.todaysound_server.domain.url.entity.Url; +import com.todaysound.todaysound_server.domain.url.repository.UrlRepository; +import com.todaysound.todaysound_server.domain.user.entity.User; +import com.todaysound.todaysound_server.domain.user.entity.UserType; +import com.todaysound.todaysound_server.domain.user.repository.UserRepository; +import com.todaysound.todaysound_server.global.dto.PageRequest; +import com.todaysound.todaysound_server.support.ServiceTestSupport; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.util.ReflectionTestUtils; + +class AlarmQueryServiceTest extends ServiceTestSupport { + + @Autowired + private UrlRepository urlRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private SubscriptionRepository subscriptionRepository; + + @Autowired + private SummaryRepository summaryRepository; + + @Autowired + private AlarmQueryService alarmQueryService; + +// @Test +// void 최근_알람을_조회한다() { +// // given +// Url url = Url.create("http://example.com", "Example Site"); +// urlRepository.save(url); +// +// User user = User.create("userId1", "hashedSecret", "fingerPrint", UserType.USER, true, "plainSecret"); +// userRepository.save(user); +// +// Subscription subscription = Subscription.create(url, true, "alias", user, "lastSeenPostId"); +// subscriptionRepository.save(subscription); +// +// Summary summary1 = Summary.create("hash1", "title1", "content1", "postUrl1", "postDate1", false, subscription); +// Summary summary2 = Summary.create("hash2", "title2", "content2", "postUrl2", "postDate2", false, subscription); +// Summary summary3 = Summary.create("hash3", "title3", "content3", "postUrl3", "postDate3", false, subscription); +// Summary summary4 = Summary.create("hash4", "title4", "content4", "postUrl4", "postDate4", false, subscription); +// ReflectionTestUtils.setField(summary4, "updatedAt", LocalDateTime.now().minusDays(1)); +// ReflectionTestUtils.setField(summary3, "updatedAt", LocalDateTime.now().minusDays(2)); +// ReflectionTestUtils.setField(summary2, "updatedAt", LocalDateTime.now().minusDays(3)); +// ReflectionTestUtils.setField(summary1, "updatedAt", LocalDateTime.now().minusDays(4)); +// summaryRepository.saveAll(List.of(summary1, summary2, summary3, summary4)); +// +// // when +// List result = alarmQueryService.getRecentAlarms(new PageRequest(0, 5), user.getUserId(), user.getHashedSecret()); +// +// // then +// assertThat(result.get(0).alias()).isEqualTo("title4"); +// assertThat(result.get(1).alias()).isEqualTo("title3"); +// assertThat(result.get(2).alias()).isEqualTo("title2"); +// assertThat(result.get(3).alias()).isEqualTo("title1"); +// } + +} \ No newline at end of file diff --git a/src/test/java/com/todaysound/todaysound_server/support/ServiceTestSupport.java b/src/test/java/com/todaysound/todaysound_server/support/ServiceTestSupport.java index 3fed968..418aba6 100644 --- a/src/test/java/com/todaysound/todaysound_server/support/ServiceTestSupport.java +++ b/src/test/java/com/todaysound/todaysound_server/support/ServiceTestSupport.java @@ -1,9 +1,13 @@ package com.todaysound.todaysound_server.support; +import com.todaysound.todaysound_server.support.isolation.DatabaseIsolation; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; @SpringBootTest +@DatabaseIsolation +@ActiveProfiles("ci") public abstract class ServiceTestSupport { diff --git a/src/test/java/com/todaysound/todaysound_server/support/isolation/DatabaseIsolation.java b/src/test/java/com/todaysound/todaysound_server/support/isolation/DatabaseIsolation.java new file mode 100644 index 0000000..73c0ded --- /dev/null +++ b/src/test/java/com/todaysound/todaysound_server/support/isolation/DatabaseIsolation.java @@ -0,0 +1,14 @@ +package com.todaysound.todaysound_server.support.isolation; + +import org.junit.jupiter.api.extension.ExtendWith; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(DatabaseIsolationExtension.class) +public @interface DatabaseIsolation { +} diff --git a/src/test/java/com/todaysound/todaysound_server/support/isolation/DatabaseIsolationExtension.java b/src/test/java/com/todaysound/todaysound_server/support/isolation/DatabaseIsolationExtension.java new file mode 100644 index 0000000..7a04876 --- /dev/null +++ b/src/test/java/com/todaysound/todaysound_server/support/isolation/DatabaseIsolationExtension.java @@ -0,0 +1,20 @@ +package com.todaysound.todaysound_server.support.isolation; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +class DatabaseIsolationExtension implements AfterEachCallback { + + @Override + public void afterEach(ExtensionContext context) { + DatabaseManager databaseManager = getDatabaseManager(context); + databaseManager.truncateTables(); + } + + private DatabaseManager getDatabaseManager(ExtensionContext context) { + return (DatabaseManager) SpringExtension + .getApplicationContext(context) + .getBean("databaseManager"); + } +} diff --git a/src/test/java/com/todaysound/todaysound_server/support/isolation/DatabaseManager.java b/src/test/java/com/todaysound/todaysound_server/support/isolation/DatabaseManager.java new file mode 100644 index 0000000..679e061 --- /dev/null +++ b/src/test/java/com/todaysound/todaysound_server/support/isolation/DatabaseManager.java @@ -0,0 +1,28 @@ +package com.todaysound.todaysound_server.support.isolation; + +import jakarta.persistence.EntityManager; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Component +@Transactional +class DatabaseManager { + + private final EntityManager entityManager; + private final List tableNames; + + public DatabaseManager(EntityManager entityManager, TableNameExtractor tableNameExtractor) { + this.entityManager = entityManager; + this.tableNames = tableNameExtractor.getNames(); + } + + public void truncateTables() { + entityManager.createNativeQuery("SET foreign_key_checks = 0").executeUpdate(); + for (String tableName : tableNames) { + entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate(); + } + entityManager.createNativeQuery("SET foreign_key_checks = 1").executeUpdate(); + } +} diff --git a/src/test/java/com/todaysound/todaysound_server/support/isolation/MySqlTableNameExtractor.java b/src/test/java/com/todaysound/todaysound_server/support/isolation/MySqlTableNameExtractor.java new file mode 100644 index 0000000..f7bb2ca --- /dev/null +++ b/src/test/java/com/todaysound/todaysound_server/support/isolation/MySqlTableNameExtractor.java @@ -0,0 +1,28 @@ +package com.todaysound.todaysound_server.support.isolation; + +import jakarta.persistence.EntityManager; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Profile("ci") +class MySqlTableNameExtractor implements TableNameExtractor{ + + private final EntityManager entityManager; + + public MySqlTableNameExtractor(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Override + @SuppressWarnings("unchecked") + public List getNames() { + return entityManager.createNativeQuery("SHOW TABLES") + .getResultList() + .stream() + .filter(name -> !name.equals("flyway_schema_history")) + .toList(); + } +} diff --git a/src/test/java/com/todaysound/todaysound_server/support/isolation/TableNameExtractor.java b/src/test/java/com/todaysound/todaysound_server/support/isolation/TableNameExtractor.java new file mode 100644 index 0000000..e139634 --- /dev/null +++ b/src/test/java/com/todaysound/todaysound_server/support/isolation/TableNameExtractor.java @@ -0,0 +1,8 @@ +package com.todaysound.todaysound_server.support.isolation; + +import java.util.List; + +interface TableNameExtractor { + + List getNames(); +} diff --git a/src/test/resources/application-ci.yml b/src/test/resources/application-ci.yml index 75f3e3f..3883fe3 100644 --- a/src/test/resources/application-ci.yml +++ b/src/test/resources/application-ci.yml @@ -5,7 +5,7 @@ spring: username: root password: password flyway: - locations: classpath:db/migration/prod + locations: classpath:db/migration jpa: show-sql: false properties: @@ -24,3 +24,7 @@ jwt: cookie: valid-time: 604800 # 60 * 60 * 24 * 7 (일주일) name: HCC_SES + + +fcm: + secret-string: ${FCM_JSON:} \ No newline at end of file