diff --git a/src/main/java/com/sofa/linkiving/domain/link/ai/AiSummaryClient.java b/src/main/java/com/sofa/linkiving/domain/link/ai/AiSummaryClient.java new file mode 100644 index 00000000..afd3449a --- /dev/null +++ b/src/main/java/com/sofa/linkiving/domain/link/ai/AiSummaryClient.java @@ -0,0 +1,22 @@ +package com.sofa.linkiving.domain.link.ai; + +import com.sofa.linkiving.domain.link.enums.Format; + +public interface AiSummaryClient { + /** + * AI 서버에 요약 요청을 보냅니다. + * @param linkId 링크 ID + * @param url 요약할 URL + * @param format 요약 모드 + * @return 요약된 텍스트 + */ + String generateSummary(Long linkId, String url, Format format); + + /** + * 기존 요약과 신규 요약 내용을 비교합니다. + * @param existingSummary 기존 요약 + * @param newSummary 신규 요약 + * @return 요약 비교 정보 + */ + String comparisonSummary(String existingSummary, String newSummary); +} diff --git a/src/main/java/com/sofa/linkiving/domain/link/ai/MockAiSummaryClient.java b/src/main/java/com/sofa/linkiving/domain/link/ai/MockAiSummaryClient.java new file mode 100644 index 00000000..55960d4d --- /dev/null +++ b/src/main/java/com/sofa/linkiving/domain/link/ai/MockAiSummaryClient.java @@ -0,0 +1,41 @@ +package com.sofa.linkiving.domain.link.ai; + +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +import com.sofa.linkiving.domain.link.enums.Format; + +@Component +@Primary +public class MockAiSummaryClient implements AiSummaryClient { + + @Override + public String generateSummary(Long linkId, String url, Format format) { + + if (format == Format.DETAILED) { + return """ + [자세한 요약 (Mock)] + OpenFeign 도입을 대비하여 Interface 기반 설계를 적용했습니다. + 1. AiSummaryClient 인터페이스를 정의하여 의존성을 역전시켰습니다. + 2. 현재는 MockAiSummaryClient가 동작하지만, 추후 실제 구현체로 교체하기 쉽습니다. + 3. 비즈니스 로직은 AI 서버의 통신 방식(HTTP, gRPC 등)에 영향을 받지 않습니다. + """; + } else { + return """ + [간결한 요약 (Mock)] + OpenFeign 도입을 위해 인터페이스 패턴을 적용하여, 코드 수정 없이 구현체 교체가 가능한 확장성 있는 구조를 만들었습니다. + """; + } + } + + @Override + public String comparisonSummary(String existingSummary, String newSummary) { + return """ + [변경 사항 분석] + 기존 요약 대비 다음 내용이 보강되었습니다: + - AI 아키텍처 설계 방식에 대한 구체적인 설명 추가 + - OpenFeign 도입의 이점 명시 + (이 내용은 Mock 데이터이며, 실제 AI는 두 텍스트의 차이를 분석하여 제공합니다.) + """; + } +} diff --git a/src/main/java/com/sofa/linkiving/domain/link/controller/LinkApi.java b/src/main/java/com/sofa/linkiving/domain/link/controller/LinkApi.java index 9c947187..24515ce2 100644 --- a/src/main/java/com/sofa/linkiving/domain/link/controller/LinkApi.java +++ b/src/main/java/com/sofa/linkiving/domain/link/controller/LinkApi.java @@ -13,11 +13,14 @@ import com.sofa.linkiving.domain.link.dto.request.LinkUpdateReq; import com.sofa.linkiving.domain.link.dto.response.LinkDuplicateCheckRes; import com.sofa.linkiving.domain.link.dto.response.LinkRes; +import com.sofa.linkiving.domain.link.dto.response.RecreateSummaryResponse; +import com.sofa.linkiving.domain.link.enums.Format; import com.sofa.linkiving.domain.member.entity.Member; import com.sofa.linkiving.global.common.BaseResponse; import com.sofa.linkiving.security.annotation.AuthMember; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -74,4 +77,11 @@ ResponseEntity> updateMemo( @Valid @RequestBody LinkMemoUpdateReq request, @AuthMember Member member ); + + @Operation(summary = "요약 재생성", description = "요약을 재생성 하고 신규 요약 기존 요약, 기존 및 신규 요약 비교 정보을 제공합니다.") + BaseResponse recreateSummary( + @PathVariable Long id, + @Valid @RequestParam @Schema(description = "요청 형식(CONCISE: 간결하게, DETAILED:자세하게)") Format format, + @AuthMember Member member + ); } diff --git a/src/main/java/com/sofa/linkiving/domain/link/controller/LinkController.java b/src/main/java/com/sofa/linkiving/domain/link/controller/LinkController.java index 3802cc2f..e89aea8e 100644 --- a/src/main/java/com/sofa/linkiving/domain/link/controller/LinkController.java +++ b/src/main/java/com/sofa/linkiving/domain/link/controller/LinkController.java @@ -21,6 +21,8 @@ import com.sofa.linkiving.domain.link.dto.request.LinkUpdateReq; import com.sofa.linkiving.domain.link.dto.response.LinkDuplicateCheckRes; import com.sofa.linkiving.domain.link.dto.response.LinkRes; +import com.sofa.linkiving.domain.link.dto.response.RecreateSummaryResponse; +import com.sofa.linkiving.domain.link.enums.Format; import com.sofa.linkiving.domain.link.facade.LinkFacade; import com.sofa.linkiving.domain.member.entity.Member; import com.sofa.linkiving.global.common.BaseResponse; @@ -129,4 +131,15 @@ public ResponseEntity> updateMemo( LinkRes response = linkFacade.updateMemo(id, member, request.memo()); return ResponseEntity.ok(BaseResponse.success(response, "메모 수정 완료")); } + + @Override + @GetMapping("/{id}/summary") + public BaseResponse recreateSummary( + @PathVariable Long id, + @Valid @RequestParam Format format, + @AuthMember Member member + ) { + RecreateSummaryResponse response = linkFacade.recreateSummary(member, id, format); + return BaseResponse.success(response, "요약 재성성 완료"); + } } diff --git a/src/main/java/com/sofa/linkiving/domain/link/dto/response/RecreateSummaryResponse.java b/src/main/java/com/sofa/linkiving/domain/link/dto/response/RecreateSummaryResponse.java new file mode 100644 index 00000000..807b662b --- /dev/null +++ b/src/main/java/com/sofa/linkiving/domain/link/dto/response/RecreateSummaryResponse.java @@ -0,0 +1,15 @@ +package com.sofa.linkiving.domain.link.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Builder +public record RecreateSummaryResponse( + @Schema(description = "기존 요약") + String existingSummary, + @Schema(description = "신규 요약") + String newSummary, + @Schema(description = "비교 정보") + String comparison +) { +} diff --git a/src/main/java/com/sofa/linkiving/domain/link/error/LinkErrorCode.java b/src/main/java/com/sofa/linkiving/domain/link/error/LinkErrorCode.java index d05c5a60..878b39bf 100644 --- a/src/main/java/com/sofa/linkiving/domain/link/error/LinkErrorCode.java +++ b/src/main/java/com/sofa/linkiving/domain/link/error/LinkErrorCode.java @@ -12,7 +12,8 @@ public enum LinkErrorCode implements ErrorCode { LINK_NOT_FOUND(HttpStatus.NOT_FOUND, "L-001", "링크를 찾을 수 없습니다."), - DUPLICATE_URL(HttpStatus.BAD_REQUEST, "L-002", "이미 저장된 URL입니다."); + DUPLICATE_URL(HttpStatus.BAD_REQUEST, "L-002", "이미 저장된 URL입니다."), + SUMMARY_NOT_FOUND(HttpStatus.BAD_REQUEST, "L-010", "요약 정보를 찾을 수 없습니다."); private final HttpStatus status; private final String code; diff --git a/src/main/java/com/sofa/linkiving/domain/link/facade/LinkFacade.java b/src/main/java/com/sofa/linkiving/domain/link/facade/LinkFacade.java index 5527e8fc..b6fed790 100644 --- a/src/main/java/com/sofa/linkiving/domain/link/facade/LinkFacade.java +++ b/src/main/java/com/sofa/linkiving/domain/link/facade/LinkFacade.java @@ -3,19 +3,25 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import com.sofa.linkiving.domain.link.dto.response.LinkDuplicateCheckRes; import com.sofa.linkiving.domain.link.dto.response.LinkRes; +import com.sofa.linkiving.domain.link.dto.response.RecreateSummaryResponse; +import com.sofa.linkiving.domain.link.enums.Format; import com.sofa.linkiving.domain.link.service.LinkService; +import com.sofa.linkiving.domain.link.service.SummaryService; import com.sofa.linkiving.domain.member.entity.Member; import lombok.RequiredArgsConstructor; @Service +@Transactional @RequiredArgsConstructor public class LinkFacade { private final LinkService linkService; + private final SummaryService summaryService; public LinkRes createLink(Member member, String url, String title, String memo, String imageUrl) { return linkService.createLink(member, url, title, memo, imageUrl); @@ -48,4 +54,21 @@ public Page getLinkList(Member member, Pageable pageable) { public LinkDuplicateCheckRes checkDuplicate(Member member, String url) { return linkService.checkDuplicate(member, url); } + + @Transactional(readOnly = true) + public RecreateSummaryResponse recreateSummary(Member member, Long linkId, Format format) { + + String url = linkService.getLink(linkId, member).url(); + + String existingSummary = summaryService.getSummary(linkId).getContent(); + String newSummary = summaryService.createSummary(linkId, url, format); + + String comparison = summaryService.comparisonSummary(existingSummary, newSummary); + + return RecreateSummaryResponse.builder() + .existingSummary(existingSummary) + .newSummary(newSummary) + .comparison(comparison) + .build(); + } } diff --git a/src/main/java/com/sofa/linkiving/domain/link/repository/SummaryRepository.java b/src/main/java/com/sofa/linkiving/domain/link/repository/SummaryRepository.java new file mode 100644 index 00000000..c28d7d57 --- /dev/null +++ b/src/main/java/com/sofa/linkiving/domain/link/repository/SummaryRepository.java @@ -0,0 +1,10 @@ +package com.sofa.linkiving.domain.link.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.sofa.linkiving.domain.link.entity.Summary; + +@Repository +public interface SummaryRepository extends JpaRepository { +} diff --git a/src/main/java/com/sofa/linkiving/domain/link/service/SummaryQueryService.java b/src/main/java/com/sofa/linkiving/domain/link/service/SummaryQueryService.java new file mode 100644 index 00000000..85ce153f --- /dev/null +++ b/src/main/java/com/sofa/linkiving/domain/link/service/SummaryQueryService.java @@ -0,0 +1,22 @@ +package com.sofa.linkiving.domain.link.service; + +import org.springframework.stereotype.Service; + +import com.sofa.linkiving.domain.link.entity.Summary; +import com.sofa.linkiving.domain.link.error.LinkErrorCode; +import com.sofa.linkiving.domain.link.repository.SummaryRepository; +import com.sofa.linkiving.global.error.exception.BusinessException; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class SummaryQueryService { + private final SummaryRepository summaryRepository; + + public Summary getSummary(Long linkId) { + return summaryRepository.findById(linkId).orElseThrow( + () -> new BusinessException(LinkErrorCode.SUMMARY_NOT_FOUND) + ); + } +} diff --git a/src/main/java/com/sofa/linkiving/domain/link/service/SummaryService.java b/src/main/java/com/sofa/linkiving/domain/link/service/SummaryService.java new file mode 100644 index 00000000..f867945a --- /dev/null +++ b/src/main/java/com/sofa/linkiving/domain/link/service/SummaryService.java @@ -0,0 +1,28 @@ +package com.sofa.linkiving.domain.link.service; + +import org.springframework.stereotype.Service; + +import com.sofa.linkiving.domain.link.ai.AiSummaryClient; +import com.sofa.linkiving.domain.link.entity.Summary; +import com.sofa.linkiving.domain.link.enums.Format; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class SummaryService { + private final SummaryQueryService summaryQueryService; + private final AiSummaryClient aiSummaryClient; + + public String createSummary(Long linkId, String url, Format format) { + return aiSummaryClient.generateSummary(linkId, url, format); + } + + public String comparisonSummary(String existingSummary, String newSummary) { + return aiSummaryClient.comparisonSummary(existingSummary, newSummary); + } + + public Summary getSummary(Long linkId) { + return summaryQueryService.getSummary(linkId); + } +} diff --git a/src/test/java/com/sofa/linkiving/domain/link/ai/MockAiSummaryClientTest.java b/src/test/java/com/sofa/linkiving/domain/link/ai/MockAiSummaryClientTest.java new file mode 100644 index 00000000..1bc5ce71 --- /dev/null +++ b/src/test/java/com/sofa/linkiving/domain/link/ai/MockAiSummaryClientTest.java @@ -0,0 +1,44 @@ +package com.sofa.linkiving.domain.link.ai; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.sofa.linkiving.domain.link.enums.Format; + +public class MockAiSummaryClientTest { + private final MockAiSummaryClient client = new MockAiSummaryClient(); + + @Test + @DisplayName("DETAILED 포맷 요청 시 상세 요약 텍스트 반환") + void generateSummary_Detailed() { + // when + String result = client.generateSummary(1L, "url", Format.DETAILED); + + // then + assertThat(result).contains("[자세한 요약 (Mock)]"); + assertThat(result).contains("OpenFeign 도입"); + } + + @Test + @DisplayName("SIMPLE 포맷 요청 시 간결한 요약 텍스트 반환") + void generateSummary_Simple() { + // when + String result = client.generateSummary(1L, "url", Format.CONCISE); + + // then + assertThat(result).contains("[간결한 요약 (Mock)]"); + } + + @Test + @DisplayName("comparisonSummary 호출 시 변경 사항 분석 텍스트 반환") + void comparisonSummary() { + // when + String result = client.comparisonSummary("old", "new"); + + // then + assertThat(result).contains("[변경 사항 분석]"); + assertThat(result).contains("보강되었습니다"); + } +} diff --git a/src/test/java/com/sofa/linkiving/domain/link/facade/LinkFacadeTest.java b/src/test/java/com/sofa/linkiving/domain/link/facade/LinkFacadeTest.java new file mode 100644 index 00000000..bc090934 --- /dev/null +++ b/src/test/java/com/sofa/linkiving/domain/link/facade/LinkFacadeTest.java @@ -0,0 +1,74 @@ +package com.sofa.linkiving.domain.link.facade; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.sofa.linkiving.domain.link.dto.response.LinkRes; +import com.sofa.linkiving.domain.link.dto.response.RecreateSummaryResponse; +import com.sofa.linkiving.domain.link.entity.Summary; +import com.sofa.linkiving.domain.link.enums.Format; +import com.sofa.linkiving.domain.link.service.LinkService; +import com.sofa.linkiving.domain.link.service.SummaryService; +import com.sofa.linkiving.domain.member.entity.Member; + +@ExtendWith(MockitoExtension.class) +public class LinkFacadeTest { + + @InjectMocks + private LinkFacade linkFacade; + + @Mock + private LinkService linkService; + + @Mock + private SummaryService summaryService; + + @Test + @DisplayName("요약 재생성 및 비교 분석 성공 테스트") + void shouldReturnRecreateSummaryResponseWhenRecreateSummary() { + // given + Long linkId = 1L; + Member member = mock(Member.class); // Member 객체 Mock + Format format = Format.DETAILED; + String url = "https://example.com"; + String existingSummaryBody = "기존 요약 내용입니다."; + String newSummaryBody = "새로운 상세 요약 내용입니다."; + String comparisonBody = "기존 대비 상세 내용이 추가되었습니다."; + + // 1. LinkService Mocking (URL 가져오기) + LinkRes mockLinkRes = mock(LinkRes.class); + given(mockLinkRes.url()).willReturn(url); + given(linkService.getLink(linkId, member)).willReturn(mockLinkRes); + + // 2. SummaryService (기존 요약 가져오기) + Summary mockSummary = mock(Summary.class); + given(mockSummary.getContent()).willReturn(existingSummaryBody); + given(summaryService.getSummary(linkId)).willReturn(mockSummary); + + // 3. SummaryService (새 요약 생성 및 비교) + given(summaryService.createSummary(linkId, url, format)).willReturn(newSummaryBody); + given(summaryService.comparisonSummary(existingSummaryBody, newSummaryBody)).willReturn(comparisonBody); + + // when + RecreateSummaryResponse response = linkFacade.recreateSummary(member, linkId, format); + + // then + assertThat(response).isNotNull(); + assertThat(response.existingSummary()).isEqualTo(existingSummaryBody); + assertThat(response.newSummary()).isEqualTo(newSummaryBody); + assertThat(response.comparison()).isEqualTo(comparisonBody); + + // verify + verify(linkService).getLink(linkId, member); + verify(summaryService).getSummary(linkId); + verify(summaryService).createSummary(linkId, url, format); + verify(summaryService).comparisonSummary(existingSummaryBody, newSummaryBody); + } +} diff --git a/src/test/java/com/sofa/linkiving/domain/link/integration/LinkApiIntegrationTest.java b/src/test/java/com/sofa/linkiving/domain/link/integration/LinkApiIntegrationTest.java index c8648ab7..7fa631b1 100644 --- a/src/test/java/com/sofa/linkiving/domain/link/integration/LinkApiIntegrationTest.java +++ b/src/test/java/com/sofa/linkiving/domain/link/integration/LinkApiIntegrationTest.java @@ -1,8 +1,10 @@ package com.sofa.linkiving.domain.link.integration; import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import org.junit.jupiter.api.BeforeEach; @@ -14,17 +16,22 @@ import org.springframework.http.MediaType; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.annotation.Transactional; import com.fasterxml.jackson.databind.ObjectMapper; +import com.sofa.linkiving.domain.link.ai.AiSummaryClient; import com.sofa.linkiving.domain.link.dto.request.LinkCreateReq; import com.sofa.linkiving.domain.link.dto.request.LinkMemoUpdateReq; import com.sofa.linkiving.domain.link.dto.request.LinkTitleUpdateReq; import com.sofa.linkiving.domain.link.dto.request.LinkUpdateReq; import com.sofa.linkiving.domain.link.entity.Link; +import com.sofa.linkiving.domain.link.entity.Summary; +import com.sofa.linkiving.domain.link.enums.Format; import com.sofa.linkiving.domain.link.error.LinkErrorCode; import com.sofa.linkiving.domain.link.repository.LinkRepository; +import com.sofa.linkiving.domain.link.repository.SummaryRepository; import com.sofa.linkiving.domain.member.entity.Member; import com.sofa.linkiving.domain.member.enums.Role; import com.sofa.linkiving.domain.member.repository.MemberRepository; @@ -49,8 +56,14 @@ public class LinkApiIntegrationTest { @Autowired MemberRepository memberRepository; - private Member testMember; + @Autowired + private SummaryRepository summaryRepository; + + @MockitoBean + private AiSummaryClient aiSummaryClient; + + private Member testMember; private Member otherMember; private UserDetails testUserDetails; private UserDetails otherUserDetails; @@ -558,4 +571,47 @@ void shouldFailWhenUrlExceedsMaxLength() throws Exception { ) .andExpect(status().isBadRequest()); } + + @Test + @DisplayName("요약 재생성 요청 시 DB 조회 및 AI 클라이언트 호출 후 결과 반환 및 200 OK 응답") + void shouldRecreateSummarySuccessfully() throws Exception { + + //given + Link savedLink = linkRepository.save(Link.builder() + .member(testMember) + .url("https://example.com/article") + .title("테스트 링크") + .build()); + + Format format = Format.DETAILED; + Long linkId = savedLink.getId(); + + summaryRepository.save(Summary.builder() + .link(savedLink) + .content("기존 요약입니다.") + .build()); + + String newSummaryText = "새로 생성된 상세 요약입니다."; + String comparisonText = "내용이 더 보강되었습니다."; + + given(aiSummaryClient.generateSummary(eq(linkId), anyString(), eq(format))) + .willReturn(newSummaryText); + + given(aiSummaryClient.comparisonSummary(anyString(), eq(newSummaryText))) + .willReturn(comparisonText); + + // when & then + mockMvc.perform(get(BASE_URL + "/{id}/summary", linkId) + .param("format", "DETAILED") + .with(csrf()) + .with(user(testUserDetails)) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("요약 재성성 완료")) + .andExpect(jsonPath("$.data.existingSummary").value("기존 요약입니다.")) + .andExpect(jsonPath("$.data.newSummary").value(newSummaryText)) + .andExpect(jsonPath("$.data.comparison").value(comparisonText)); + } } diff --git a/src/test/java/com/sofa/linkiving/domain/link/service/SummaryQueryServiceTest.java b/src/test/java/com/sofa/linkiving/domain/link/service/SummaryQueryServiceTest.java new file mode 100644 index 00000000..31697030 --- /dev/null +++ b/src/test/java/com/sofa/linkiving/domain/link/service/SummaryQueryServiceTest.java @@ -0,0 +1,59 @@ +package com.sofa.linkiving.domain.link.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.sofa.linkiving.domain.link.entity.Summary; +import com.sofa.linkiving.domain.link.error.LinkErrorCode; +import com.sofa.linkiving.domain.link.repository.SummaryRepository; +import com.sofa.linkiving.global.error.exception.BusinessException; + +@ExtendWith(MockitoExtension.class) +public class SummaryQueryServiceTest { + @InjectMocks + private SummaryQueryService summaryQueryService; + + @Mock + private SummaryRepository summaryRepository; + + @Test + @DisplayName("요약 정보 조회 성공") + void shouldReturnSummaryWhenSummaryExists() { + // given + Long linkId = 1L; + Summary mockSummary = mock(Summary.class); // Summary 엔티티 Mock + given(summaryRepository.findById(linkId)).willReturn(Optional.of(mockSummary)); + + // when + Summary result = summaryQueryService.getSummary(linkId); + + // then + assertThat(result).isNotNull(); + assertThat(result).isEqualTo(mockSummary); + verify(summaryRepository).findById(linkId); + } + + @Test + @DisplayName("요약 정보를 찾을 수 없을 때 BusinessException(SUMMARY_NOT_FOUND) 발생") + void shouldThrowBusinessExceptionWhenSummaryNotFound() { + // given + Long linkId = 999L; + given(summaryRepository.findById(linkId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> summaryQueryService.getSummary(linkId)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("errorCode", LinkErrorCode.SUMMARY_NOT_FOUND); + + verify(summaryRepository).findById(linkId); + } +} diff --git a/src/test/java/com/sofa/linkiving/domain/link/service/SummaryServiceTest.java b/src/test/java/com/sofa/linkiving/domain/link/service/SummaryServiceTest.java new file mode 100644 index 00000000..0ef8695f --- /dev/null +++ b/src/test/java/com/sofa/linkiving/domain/link/service/SummaryServiceTest.java @@ -0,0 +1,81 @@ +package com.sofa.linkiving.domain.link.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.sofa.linkiving.domain.link.ai.AiSummaryClient; +import com.sofa.linkiving.domain.link.entity.Summary; +import com.sofa.linkiving.domain.link.enums.Format; + +@ExtendWith(MockitoExtension.class) +public class SummaryServiceTest { + @InjectMocks + private SummaryService summaryService; + + @Mock + private SummaryQueryService summaryQueryService; + + @Mock + private AiSummaryClient aiSummaryClient; + + @Test + @DisplayName("createSummary 호출 시 AiSummaryClient에게 위임한다") + void shouldCallGenerateSummaryWhenCreateSummary() { + // given + Long linkId = 1L; + String url = "https://example.com"; + Format format = Format.CONCISE; + String expectedResult = "Generated Summary"; + + given(aiSummaryClient.generateSummary(linkId, url, format)).willReturn(expectedResult); + + // when + String result = summaryService.createSummary(linkId, url, format); + + // then + assertThat(result).isEqualTo(expectedResult); + verify(aiSummaryClient).generateSummary(linkId, url, format); + } + + @Test + @DisplayName("comparisonSummary 호출 시 AiSummaryClient에게 위임한다") + void shouldCallComparisonSummaryWhenComparisonSummary() { + // given + String oldSummary = "old"; + String newSummary = "new"; + String expectedResult = "Comparison Result"; + + given(aiSummaryClient.comparisonSummary(oldSummary, newSummary)).willReturn(expectedResult); + + // when + String result = summaryService.comparisonSummary(oldSummary, newSummary); + + // then + assertThat(result).isEqualTo(expectedResult); + verify(aiSummaryClient).comparisonSummary(oldSummary, newSummary); + } + + @Test + @DisplayName("getSummary 호출 시 SummaryQueryService에게 위임한다") + void shouldCallGetSummaryWhenGetSummary() { + // given + Long linkId = 1L; + Summary mockSummary = mock(Summary.class); + + given(summaryQueryService.getSummary(linkId)).willReturn(mockSummary); + + // when + Summary result = summaryService.getSummary(linkId); + + // then + assertThat(result).isEqualTo(mockSummary); + verify(summaryQueryService).getSummary(linkId); + } +}