Skip to content

Commit

Permalink
잔여 테스트 케이스 작성 (#115)
Browse files Browse the repository at this point in the history
* feat: 연관 대화 주제 조회 API, 대화 주제 ID 명세 추가(#114)

path variable인 topicId(대화 주제 ID) 명세 추가

* test: 연관 대화 주제 조회, 대화 주제 상세 조회 API 테스트 추가(#114)

* test: JWT 생성, 파싱 기능 테스트 작성(#114)

* test: ID 기반 대화 주제 조회 기능 테스트 작성(#114)

* feat: 코드 정렬 수정(#114)

* test: 스크랩 제목 수정 API 단위 테스트 작성(#114)

* feat: 명세 오타 수정(#114)

* feat: 코드 정렬 수정(#114)

* test: 연관 대화 주제 조회, 대화 주제 상세 조회 단위 테스트 작성(#114)

* test: 사용자 대화 주제 스크랩 여부 조회 로직, 단위 테스트 작성(#114)

* chore: llm 패키지 테스트 커버리지 측정 제외 설정(#114)

철저히 외부 서비스와의 상호작용을 위해 정의된 클래스, 테스트 대상에서 제외

* test: fcm 알림 전송 기능, 단위 테스트 작성(#114)

* feat: 코드 정렬 수정(#114)

* test: 카카오 소셜 로그인 API 단위 테스트 작성(#114)

* test: 카카오 소셜 로그인 단위 테스트 작성, 기타 TC 추가(#114)

- 카카오 소셜 로그인 단위 테스트 작성
- access token refresh, refresh token 불일치 TC 추가

* feat: 스크랩과 알림 연관관계, 다대일로 변경(#114)

- 하나의 스크랩에 대해 알림을 여러 개 설정할 수 있음
- 이를 제대로 반영하기 위해 기존의 일대일 관계, 다대일로 수정

* feat: 스트림내 변수명 적절히 수정(#114)

알림 목록 조회 로직내 변수명 적절히 수정

* test: 알림 목록 조회 쿼리, 단위 테스트 작성(#114)

- 추가 내용
    - 알림 목록 조회 쿼리에 대한 단위 테스트 작성
    - 예정 알림 목록 조회 쿼리에 대한 단위 테스트 작성
- LocalTime 타입 데이터를 DB에서 조회한 후 코드에서 비교 시, DB 시간 타입의 정밀도
  차이로 인해 밀리초 단위 이하에서 비교가 어려운 문제 발생
- 이를 해결하기 위해 시간 비교를 초 단위로 수행하는 보조 비교 로직 구현

* feat: LocalDateTime 제공 유틸 기능 개발(#114)

일자/시간 데이터를 제공하는 유틸 클래스 구현

* feat: TimeProvider를 통해 송신 일시 제공받도록 로직 수정(#114)

- 기존 로직은 LocalDateTime.now()를 통해 송신 일시 결정
- 이 구조는 테스트가 어려운, 블랙 박스에 가까운 구조
- 테스트 가능하도록 TimeProvider를 이용하는 형식으로 변경

* test: 예정된 알림 전송 로직, 단위 테스트 작성(#114)
  • Loading branch information
Minjae-An authored Dec 4, 2024
1 parent d7912f7 commit eab3b85
Show file tree
Hide file tree
Showing 20 changed files with 1,049 additions and 119 deletions.
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,10 @@ def jacocoExcludePatterns = [
'**/enums/**',
'**/event/**',
'**/converter/**',
'**/llm/**',
'**/test/**'
]
for (qPattern in '**/QA' .. '**/QZ') {
for (qPattern in '**/QA'..'**/QZ') {
jacocoExcludePatterns.add(qPattern + '*')
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
package org.example.tokpik_be.login.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.example.tokpik_be.login.dto.request.AccessTokenRefreshRequest;
import org.example.tokpik_be.login.dto.request.LoginByKakaoRequest;
import org.example.tokpik_be.login.dto.response.AccessTokenRefreshResponse;
Expand All @@ -15,6 +10,12 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@Tag(name = "Login API", description = "로그인 관련 API")
@RestController
@RequiredArgsConstructor
Expand All @@ -25,8 +26,7 @@ public class LoginController {
@Operation(summary = "카카오 소셜 로그인", description = "카카오 인카 코드 바탕 소셜 로그인 수행")
@ApiResponse(responseCode = "200", description = "카카오 소셜 로그인 성공")
@PostMapping("/login/kakao")
public ResponseEntity<LoginResponse> kakaoLogin(
@RequestBody @Valid LoginByKakaoRequest request) {
public ResponseEntity<LoginResponse> kakaoLogin(@RequestBody @Valid LoginByKakaoRequest request) {

LoginResponse response = loginCommandService.kakaoLogin(request);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import java.time.LocalDate;
import java.time.LocalTime;
Expand Down Expand Up @@ -50,7 +49,7 @@ public class Notification extends BaseTimeEntity {
@JoinColumn(name = "user_id")
private User user;

@OneToOne
@ManyToOne
@JoinColumn(name = "scrap_id")
private Scrap scrap;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import static org.example.tokpik_be.notification.domain.QNotification.notification;
import static org.example.tokpik_be.notification.domain.QNotificationTalkTopic.notificationTalkTopic;
import static org.example.tokpik_be.type.domain.QTopicType.topicType;
import static org.example.tokpik_be.talk_topic.domain.QTalkTopic.talkTopic;
import static org.example.tokpik_be.type.domain.QTopicType.topicType;
import static org.example.tokpik_be.user.domain.QUser.user;

import com.querydsl.core.Tuple;
Expand Down Expand Up @@ -93,11 +93,13 @@ public NotificationsResponse getNotifications(long userId, Long cursorId, int pa
int toIndex = Math.min(tuples.size(), 4);
List<NotificationTalkTopicTypeResponse> talkTopicTypeResponses = tuples
.stream()
.sorted(Comparator.comparing(r -> r.get(notificationTalkTopic.id)))
.sorted(
Comparator.comparing(response -> response.get(notificationTalkTopic.id)))
.toList().subList(0, toIndex)
.stream()
.map(r -> new NotificationTalkTopicTypeResponse(r.get(topicType.id),
r.get(topicType.content)))
.map(response -> new NotificationTalkTopicTypeResponse(
response.get(topicType.id),
response.get(topicType.content)))
.toList();

Tuple tuple = tuples.get(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.example.tokpik_be.talk_topic.domain.TalkTopic;
import org.example.tokpik_be.user.domain.User;
import org.example.tokpik_be.user.service.UserQueryService;
import org.example.tokpik_be.util.TimeProvider;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
Expand All @@ -43,12 +44,7 @@ public class NotificationQueryService {

private final UserQueryService userQueryService;
private final ApplicationEventPublisher eventPublisher;

public Notification findById(long notificationId) {

return notificationRepository.findById(notificationId)
.orElseThrow(() -> new GeneralException(NotificationException.NOTIFICATION_NOT_FOUND));
}
private final TimeProvider timeProvider;

public NotificationsResponse getNotifications(long userId, Long nextCursorId) {
int pageSize = 10;
Expand Down Expand Up @@ -94,6 +90,31 @@ public NotificationDetailResponse getNotificationDetail(long userId, long notifi
talkTopicResponses);
}

public Notification findById(long notificationId) {

return notificationRepository.findById(notificationId)
.orElseThrow(() -> new GeneralException(NotificationException.NOTIFICATION_NOT_FOUND));
}

/**
* 분 간격에 따라 나눠진 시간 목록을 제공하는 로직
*
* @param from 시작 시간
* @param to 종료 시간
* @param intervalMinutes 분 간격
* @return 분 간격에 따라 시작 시간 ~ 종료 시간내 나눠진 시간 목록
*/
private List<LocalTime> generateTimeIntervals(LocalTime from, LocalTime to,
int intervalMinutes) {
List<LocalTime> timeIntervals = new ArrayList<>();

while (from.isBefore(to)) {
timeIntervals.add(from);
from = from.plusMinutes(intervalMinutes);
}

return timeIntervals;
}

/**
* 1분마다 알림 송신을 수행하는 스케줄러, 이하 과정에 따라 작업 수행 <br/> <br/>
Expand All @@ -105,7 +126,7 @@ public NotificationDetailResponse getNotificationDetail(long userId, long notifi
@Async
@Scheduled(fixedRate = 1000 * 60)
public void sendScheduledNotifications() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime now = timeProvider.provideSystemTime();

List<NotificationScheduledResponse> scheduledNotifications = queryDslNotificationRepository
.getScheduledNotifications(now);
Expand Down Expand Up @@ -148,6 +169,22 @@ private List<NotificationSendEvent> filterNotificationToSend(
.toList();
}

/**
* 송신 알림 여부 확인 로직 <br/>
* 시작 시간부터 분 간격에 따라 알림 시간대를 구했을 때 송신 일시에 해당하는 것이 있는 경우 true 반환
*
* @param notification 알림
* @param noticeTime 알림 일시(송신 일시)
* @return 알림 일시(송신 일시) 해당 여부
*/
private boolean isNoticeTime(NotificationScheduledResponse notification, LocalTime noticeTime) {
LocalTime startTime = notification.startTime();
int intervalMinutes = notification.intervalMinutes();
long minutesSinceStart = ChronoUnit.MINUTES.between(startTime, noticeTime);

return minutesSinceStart % intervalMinutes == 0;
}

/**
* 특정 알림과 연관된 DTO 그룹에서 알림 지정 순서에 따라 현재 송신할 알림을 선택하는 로직
*
Expand Down Expand Up @@ -182,40 +219,4 @@ private NotificationSendEvent selectNotificationToSend(
sendNotification.talkTopicTitle(),
sendNotification.talkTopicSubtitle());
}

/**
* 송신 알림 여부 확인 로직 <br/>
* 시작 시간부터 분 간격에 따라 알림 시간대를 구했을 때 송신 일시에 해당하는 것이 있는 경우 true 반환
*
* @param notification 알림
* @param noticeTime 알림 일시(송신 일시)
* @return 알림 일시(송신 일시) 해당 여부
*/
private boolean isNoticeTime(NotificationScheduledResponse notification, LocalTime noticeTime) {
LocalTime startTime = notification.startTime();
int intervalMinutes = notification.intervalMinutes();
long minutesSinceStart = ChronoUnit.MINUTES.between(startTime, noticeTime);

return minutesSinceStart % intervalMinutes == 0;
}

/**
* 분 간격에 따라 나눠진 시간 목록을 제공하는 로직
*
* @param from 시작 시간
* @param to 종료 시간
* @param intervalMinutes 분 간격
* @return 분 간격에 따라 시작 시간 ~ 종료 시간내 나눠진 시간 목록
*/
private List<LocalTime> generateTimeIntervals(LocalTime from, LocalTime to,
int intervalMinutes) {
List<LocalTime> timeIntervals = new ArrayList<>();

while (from.isBefore(to)) {
timeIntervals.add(from);
from = from.plusMinutes(intervalMinutes);
}

return timeIntervals;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
package org.example.tokpik_be.scrap.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.example.tokpik_be.scrap.dto.request.ScrapCreateRequest;
import org.example.tokpik_be.scrap.dto.request.ScrapUpdateTitleRequest;
import org.example.tokpik_be.scrap.dto.response.ScrapCountResponse;
Expand All @@ -25,6 +18,14 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@Tag(name = "스크랩 API", description = "스크랩 연관 API")
@RestController
@RequiredArgsConstructor
Expand Down Expand Up @@ -113,7 +114,6 @@ public ResponseEntity<Void> deleteScrapTopic(
return ResponseEntity.ok().build();
}


@Operation(summary = "스크랩 조회", description = "특정 스크랩에 포함된 대화 주제들을 조회")
@ApiResponse(responseCode = "200", description = "스크랩 조회 성공")
@GetMapping("/users/scraps/{scrapId}/topics")
Expand All @@ -134,7 +134,6 @@ public ResponseEntity<ScrapResponse> getScrapTopics(
public ResponseEntity<Void> updateScrapTitle(@RequestAttribute("userId") long userId,
@Parameter(name = "scrapId", description = "스크랩 ID", example = "1")
@PathVariable("scrapId") long scrapId,

@RequestBody @Valid ScrapUpdateTitleRequest request) {

scrapService.updateScrapTitle(userId, scrapId, request);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
package org.example.tokpik_be.talk_topic.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.example.tokpik_be.talk_topic.dto.request.TalkTopicSearchRequest;
import org.example.tokpik_be.talk_topic.dto.response.TalkTopicDetailResponse;
import org.example.tokpik_be.talk_topic.dto.response.TalkTopicsRelatedResponse;
Expand All @@ -21,6 +14,14 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@Tag(name = "대화 주제 API", description = "대화 주제 연관 API")
@RestController
@RequiredArgsConstructor
Expand All @@ -47,6 +48,7 @@ public ResponseEntity<TalkTopicsSearchResponse> searchTalkTopics(
@GetMapping("/topics/{topicId}/related")
public ResponseEntity<TalkTopicsRelatedResponse> getRelatedTalkTopics(
@RequestAttribute("userId") long userId,
@Parameter(name = "topicId", description = "대화 주제 ID", example = "1", in = ParameterIn.PATH)
@PathVariable("topicId") long topicId) {

TalkTopicsRelatedResponse response = talkTopicQueryService
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package org.example.tokpik_be.talk_topic.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;

import io.swagger.v3.oas.annotations.media.Schema;

public record TalkTopicsRelatedResponse(
@Schema(type = "array", description = "연관 대화 주제들")
List<TalkTopicRelatedResponse> talkTopics
Expand All @@ -12,7 +13,7 @@ public record TalkTopicRelatedResponse(
@Schema(type = "number", description = "대화 주제 ID", example = "1")
long topicId,

@Schema(type = "string", description = "대화 주제 종류", example = "인관관계")
@Schema(type = "string", description = "대화 주제 종류", example = "인간관계")
String type,

@Schema(type = "string", description = "대화 주제 제목", example = "쓰@껄하게 스몰톡하는 법")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.example.tokpik_be.talk_topic.service;

import java.util.List;
import lombok.RequiredArgsConstructor;

import org.example.tokpik_be.exception.GeneralException;
import org.example.tokpik_be.exception.TalkTopicException;
import org.example.tokpik_be.scrap.repository.QueryDslScrapRepository;
Expand All @@ -20,6 +20,8 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
Expand Down Expand Up @@ -51,8 +53,7 @@ public TalkTopicDetailResponse getTalkTopicDetail(long userId, long topicId) {
TalkTopic talkTopic = findById(topicId);

LLMTalkTopicDetailRequest request = LLMTalkTopicDetailRequest.from(talkTopic);
LLMTalkTopicDetailResponse llmResponse = llmApiClient
.generateTalkTopicDetail(request);
LLMTalkTopicDetailResponse llmResponse = llmApiClient.generateTalkTopicDetail(request);

List<TalkTopicDetailItemResponse> details = llmResponse.details().stream()
.map(item -> new TalkTopicDetailItemResponse(item.itemTitle(), item.itemContent()))
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/org/example/tokpik_be/util/TimeProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.example.tokpik_be.util;

import java.time.LocalDateTime;
import org.springframework.stereotype.Component;

@Component
public class TimeProvider {

public LocalDateTime provideSystemTime() {

return LocalDateTime.now();
}
}
Loading

0 comments on commit eab3b85

Please sign in to comment.