-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[feat] 포트폴리오 상세 조회 및 종목 조회 SSE 적용 (#24)
* #17 fix: 실시간 현재 주식가를 redis에 저장하는 Duration 제거 - 5초에 한번씩 계속 갱신하기 때문에 1분동안 저장하는 것은 모순된다고 판단하여 제거함 * #17 fix: 당일변동 금액 공식 수정 * #17 feat: 직전거래일 종가 조회 구현 * #17 feat: 직전거래일 가격을 관리하는 매니저 객체 구현 - 종목에 따른 직전거래일 종가를 관리하는 객체입니다. - redis에 저장된 종가를 참조합니다. * #17 fix: 당일 변동 금액 공식 변경 - 종가 - 시가 -> 시가 -> 종가로 변경 * #17 fix: 테스트 코드 수정 - 당일 변동 금액 수정에 따른 테스트 코드 수정 * #17 fix: 직전거래일 종가 조회 메소드의 리턴 타입 변경 - 티커심볼과 종가가 같이 저장될 수 있도록 Response 객체로 변경 * #17 feat: LastDayClosingPriceManager hasPrice 메소드 구현 - 해당 메소드는 티커 심볼 키값에 따른 종가값이 있는지 검사하는 메소드이다 * #17 feat: LastDayClosingPriceResponse 클래스 정의 - 해당 객체는 한국투자증권 api 서버에 직전거래일 종가를 조회에 따른 응답값을 저장하는 객체입니다. * #17 feat: 직전거래일 종가를 갱신하는 메소드 구현 - 해당 메소드는 종목의 직전거래일 종가가 없다면 한국투자증권 api 서버에 요청하여 redis 저장소에 저장하는 기능입니다 * #17 fix: PortfolioHolding 객체에 직전거래일 종가를 매개변수로 전달하도록 수정 * #17 fix: 종가 갱신을 refreshCurrentPrice 메소드로 이동 * #17 fix: 주식 전일 종가를 api 서버로부터 가져오도록 수정 * #17 docs: git submodules 추가 * docs: scret 디렉토리 삭제 * docs: application-secret.yml 파일 추가 * docs: .gitmodules 삭제 * docs: application-secret.yml 추가 * .gitmodules 제거 * docs: application-secret.yml 추가 * #17 docs: cicd 워크플로우 설정에 submodules 설정 추가 * #17 style: application-secret 변경 테스트 * #17 docs: gitmodules branch 설정 * #19 docs: analyze profile 추가 * #19 fix: 시크릿 정보 업데이트 * #16 docs: 시크릿 정보 업데이트 * #16 feat: 포트폴리오 상세 조회 및 종목 조회 sse 방식으로 변경 * #16 style: import 정리 * #16 feat: 포트폴리오를 찾지 못한 경우 예외 처리 추가 * #16 test: 포트폴리오 상세 조회 sse 예외 테스트 코드 추가 - 포트폴리오 번호가 존재하지 않는 경우 * #16 fix: emitter onCompletion 삭제 - sse 이벤트 완료시 쓰레드풀을 종료하지 않도록 합니다.
- Loading branch information
1 parent
c5af417
commit 8b2d13a
Showing
3 changed files
with
197 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,7 @@ jobs: | |
defaults: | ||
run: | ||
shell: bash | ||
|
||
steps: | ||
- uses: actions/checkout@v3 | ||
with: | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
169 changes: 169 additions & 0 deletions
169
.../java/codesquad/fineants/spring/api/portfolio_stock/PortfolioStockRestControllerTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
package codesquad.fineants.spring.api.portfolio_stock; | ||
|
||
import static org.assertj.core.api.Assertions.*; | ||
import static org.hamcrest.Matchers.*; | ||
import static org.mockito.ArgumentMatchers.any; | ||
import static org.mockito.ArgumentMatchers.*; | ||
import static org.mockito.BDDMockito.*; | ||
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.request; | ||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; | ||
|
||
import java.nio.charset.StandardCharsets; | ||
import java.time.LocalDateTime; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; | ||
import org.springframework.boot.test.mock.mockito.MockBean; | ||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; | ||
import org.springframework.test.context.ActiveProfiles; | ||
import org.springframework.test.web.servlet.MockMvc; | ||
import org.springframework.test.web.servlet.MvcResult; | ||
import org.springframework.test.web.servlet.setup.MockMvcBuilders; | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
|
||
import codesquad.fineants.domain.member.Member; | ||
import codesquad.fineants.domain.oauth.support.AuthMember; | ||
import codesquad.fineants.domain.oauth.support.AuthPrincipalArgumentResolver; | ||
import codesquad.fineants.domain.portfolio.Portfolio; | ||
import codesquad.fineants.domain.portfolio_gain_history.PortfolioGainHistory; | ||
import codesquad.fineants.domain.portfolio_holding.PortfolioHolding; | ||
import codesquad.fineants.domain.purchase_history.PurchaseHistory; | ||
import codesquad.fineants.domain.stock.Market; | ||
import codesquad.fineants.domain.stock.Stock; | ||
import codesquad.fineants.spring.api.errors.errorcode.PortfolioErrorCode; | ||
import codesquad.fineants.spring.api.errors.exception.NotFoundResourceException; | ||
import codesquad.fineants.spring.api.errors.handler.GlobalExceptionHandler; | ||
import codesquad.fineants.spring.api.kis.manager.LastDayClosingPriceManager; | ||
import codesquad.fineants.spring.api.portfolio_stock.response.PortfolioHoldingsResponse; | ||
import codesquad.fineants.spring.config.JpaAuditingConfiguration; | ||
|
||
@ActiveProfiles("test") | ||
@WebMvcTest(controllers = PortfolioStockRestController.class) | ||
@MockBean(JpaAuditingConfiguration.class) | ||
class PortfolioStockRestControllerTest { | ||
private MockMvc mockMvc; | ||
|
||
@Autowired | ||
private PortfolioStockRestController portfolioStockRestController; | ||
|
||
@Autowired | ||
private GlobalExceptionHandler globalExceptionHandler; | ||
|
||
@Autowired | ||
private MappingJackson2HttpMessageConverter converter; | ||
|
||
@Autowired | ||
private ObjectMapper objectMapper; | ||
|
||
@MockBean | ||
private AuthPrincipalArgumentResolver authPrincipalArgumentResolver; | ||
|
||
@MockBean | ||
private PortfolioStockService portfolioStockService; | ||
|
||
@MockBean | ||
private LastDayClosingPriceManager lastDayClosingPriceManager; | ||
|
||
private Member member; | ||
|
||
@BeforeEach | ||
void setup() { | ||
mockMvc = MockMvcBuilders.standaloneSetup(portfolioStockRestController) | ||
.setControllerAdvice(globalExceptionHandler) | ||
.setCustomArgumentResolvers(authPrincipalArgumentResolver) | ||
.setMessageConverters(converter) | ||
.defaultResponseCharacterEncoding(StandardCharsets.UTF_8) | ||
.alwaysDo(print()) | ||
.build(); | ||
given(authPrincipalArgumentResolver.supportsParameter(any())).willReturn(true); | ||
|
||
member = Member.builder() | ||
.id(1L) | ||
.nickname("일개미1234") | ||
.email("kim1234@gmail.com") | ||
.provider("local") | ||
.password("kim1234@") | ||
.profileUrl("profileValue") | ||
.build(); | ||
|
||
AuthMember authMember = AuthMember.from(member); | ||
|
||
given(authPrincipalArgumentResolver.resolveArgument(any(), any(), any(), any())).willReturn(authMember); | ||
} | ||
|
||
@DisplayName("사용자의 포트폴리오 상세 정보를 가져온다") | ||
@Test | ||
void readMyPortfolioStocks() throws Exception { | ||
// given | ||
long portfolioId = 1; | ||
Portfolio portfolio = Portfolio.builder() | ||
.id(1L) | ||
.name("내꿈은 워렌버핏") | ||
.securitiesFirm("토스") | ||
.budget(1000000L) | ||
.targetGain(1500000L) | ||
.maximumLoss(900000L) | ||
.member(member) | ||
.build(); | ||
Stock stock = Stock.builder() | ||
.tickerSymbol("005930") | ||
.companyName("삼성전자보통주") | ||
.companyNameEng("SamsungElectronics") | ||
.stockCode("KR7005930003") | ||
.market(Market.KOSPI) | ||
.build(); | ||
long currentPrice = 60000; | ||
PortfolioHolding portfolioHolding = PortfolioHolding.of(portfolio, stock, currentPrice); | ||
portfolioHolding.addPurchaseHistory(PurchaseHistory.builder() | ||
.purchaseDate(LocalDateTime.of(2023, 11, 1, 9, 30, 0)) | ||
.numShares(3L) | ||
.purchasePricePerShare(50000.0) | ||
.memo("첫구매") | ||
.portFolioHolding(portfolioHolding) | ||
.build()); | ||
portfolio.addPortfolioStock(portfolioHolding); | ||
|
||
PortfolioGainHistory history = PortfolioGainHistory.empty(); | ||
Map<String, Long> lastDayClosingPriceMap = Map.of("005930", 50000L); | ||
PortfolioHoldingsResponse mockResponse = PortfolioHoldingsResponse.of(portfolio, history, | ||
List.of(portfolioHolding), | ||
lastDayClosingPriceMap); | ||
given(portfolioStockService.readMyPortfolioStocks(anyLong(), any(LastDayClosingPriceManager.class))) | ||
.willReturn(mockResponse); | ||
|
||
// when & then | ||
String body = mockMvc.perform(get("/api/portfolio/{portfolioId}/holdings", portfolioId)) | ||
.andExpect(request().asyncStarted()) | ||
.andReturn() | ||
.getResponse() | ||
.getContentAsString(); | ||
assertThat(body).contains(objectMapper.writeValueAsString(mockResponse)); | ||
} | ||
|
||
@DisplayName("존재하지 않는 포트폴리오 번호를 가지고 포트폴리오 상세 정보를 가져올 수 없다") | ||
@Test | ||
void readMyPortfolioStocksWithNotFoundPortfolio() throws Exception { | ||
// given | ||
long portfolioId = 9999L; | ||
given(portfolioStockService.readMyPortfolioStocks(anyLong(), any(LastDayClosingPriceManager.class))) | ||
.willThrow(new NotFoundResourceException(PortfolioErrorCode.NOT_FOUND_PORTFOLIO)); | ||
|
||
// when | ||
MvcResult result = mockMvc.perform(get("/api/portfolio/{portfolioId}/holdings", portfolioId)) | ||
.andExpect(request().asyncStarted()) | ||
.andReturn(); | ||
|
||
// then | ||
mockMvc.perform(asyncDispatch(result)) | ||
.andExpect(status().isNotFound()) | ||
.andExpect(jsonPath("message").value(equalTo("포트폴리오를 찾을 수 없습니다"))); | ||
} | ||
} |