| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139 |
- using Domain.Entities.Stocks;
- using Domain.Entities.Stocks.ValueObject;
- using Infrastructure.Persistence;
- using ListHandler = Application.Features.Api.Stocks.GetList.Handler;
- using ListQuery = Application.Features.Api.Stocks.GetList.Query;
- using QuotesHandler = Application.Features.Api.Stocks.GetQuotes.Handler;
- using QuotesQuery = Application.Features.Api.Stocks.GetQuotes.Query;
- using SuggestHandler = Application.Features.Api.Stocks.Suggest.Handler;
- using SuggestQuery = Application.Features.Api.Stocks.Suggest.Query;
- namespace Application.Tests;
- [TestClass]
- public sealed class StockQueryHandlerTests
- {
- private static async Task<Stock> CreateStockAsync(AppDbContext db, string code, string name, StockMarket market = StockMarket.KOSPI, bool delisted = false)
- {
- var stock = Stock.Create(code, name, market, new DateOnly(2020, 1, 2));
- if (delisted)
- {
- stock.MarkDelisted(new DateOnly(2025, 12, 30));
- }
- await db.Stock.AddAsync(stock);
- await db.SaveChangesAsync(default);
- return stock;
- }
- [TestMethod]
- public async Task Suggest_NamePrefix_ReturnsUpTo10()
- {
- // 기대 행위: 이름 prefix 매칭은 최대 10건까지만 반환한다.
- using var db = TestDb.Create();
- for (var i = 1; i <= 12; i++)
- {
- await CreateStockAsync(db, $"9100{i:D2}", $"ZQA자동완성{i:D2}");
- }
- var handler = new SuggestHandler(db);
- var result = await handler.Handle(new SuggestQuery("ZQA자동완성"), default);
- Assert.AreEqual(10, result.List.Count, "prefix 매칭은 10건으로 제한된다");
- Assert.IsTrue(result.List.All(c => c.Name.StartsWith("ZQA자동완성")), "이름 prefix 만 매칭되어야 한다");
- }
- [TestMethod]
- public async Task Suggest_CodePrefix_ExcludesDelisted()
- {
- // 기대 행위: 코드 prefix 로도 매칭하되 상폐(soft-off) 종목은 제외한다.
- using var db = TestDb.Create();
- await CreateStockAsync(db, "920001", "QXB활성종목");
- await CreateStockAsync(db, "920002", "QXB상폐종목", delisted: true);
- var handler = new SuggestHandler(db);
- var result = await handler.Handle(new SuggestQuery("92000"), default);
- Assert.AreEqual(1, result.List.Count, "상폐 종목은 자동완성에서 제외된다");
- Assert.AreEqual("920001", result.List[0].Code);
- Assert.AreEqual("KOSPI", result.List[0].Market);
- }
- [TestMethod]
- public async Task Suggest_EmptyQuery_ReturnsEmpty()
- {
- // 기대 행위: 빈 검색어는 DB 조회 없이 빈 목록을 반환한다.
- using var db = TestDb.Create();
- var handler = new SuggestHandler(db);
- var result = await handler.Handle(new SuggestQuery(" "), default);
- Assert.AreEqual(0, result.List.Count);
- }
- [TestMethod]
- public async Task Quotes_ReturnsDenormClosePrice_IgnoresUnknownCodes()
- {
- // 기대 행위: Stock denorm(종가/등락률)만 읽어 반환하고, 미존재 코드는 조용히 제외한다.
- using var db = TestDb.Create();
- var stock = await CreateStockAsync(db, "930001", "QZC시세종목", StockMarket.KOSDAQ);
- stock.UpdateLastPrice(new DateOnly(2026, 7, 2), 70500, 1.25m, 420_000_000_000L);
- await CreateStockAsync(db, "930002", "QZC무시세종목");
- await db.SaveChangesAsync(default);
- var handler = new QuotesHandler(db);
- var result = await handler.Handle(new QuotesQuery(["930001", "930002", "939999"]), default);
- Assert.IsTrue(result.IsSuccess);
- Assert.AreEqual(2, result.Value.List.Count, "미존재 코드는 제외된다");
- var quoted = result.Value.List.Single(c => c.Code == "930001");
- Assert.AreEqual(70500, quoted.ClosePrice);
- Assert.AreEqual(1.25m, quoted.ChangeRate);
- var noPrice = result.Value.List.Single(c => c.Code == "930002");
- Assert.IsNull(noPrice.ClosePrice, "시세 미수집 종목은 null 로 반환된다");
- }
- [TestMethod]
- public async Task List_ReturnsPrevDayVolume_FromLatestDailyPrice()
- {
- // 기대 행위: 목록의 Volume 은 최근 거래일(LastTradingDate) StockDailyPrice 의 거래량이고, 시세 미수집 종목은 null 이다.
- using var db = TestDb.Create();
- var stock = await CreateStockAsync(db, "940001", "YVL거래량종목");
- stock.UpdateLastPrice(new DateOnly(2026, 7, 2), 70500, 1.25m, 420_000_000_000L);
- await db.StockDailyPrice.AddAsync(StockDailyPrice.Create(stock.ID, new DateOnly(2026, 7, 1), 69000, 70000, 68500, 69500, 11_000_000L, 760_000_000_000L, 500, 0.72m));
- await db.StockDailyPrice.AddAsync(StockDailyPrice.Create(stock.ID, new DateOnly(2026, 7, 2), 70000, 71000, 69800, 70500, 12_345_678L, 870_000_000_000L, 1000, 1.25m));
- await CreateStockAsync(db, "940002", "YVL무시세종목");
- await db.SaveChangesAsync(default);
- var handler = new ListHandler(db);
- var result = await handler.Handle(new ListQuery("YVL", null, 1, 20), default);
- Assert.AreEqual(2, result.Total);
- var withVolume = result.List.Single(c => c.Code == "940001");
- Assert.AreEqual(12_345_678L, withVolume.Volume, "최근 거래일의 거래량이 반환되어야 한다");
- var noPrice = result.List.Single(c => c.Code == "940002");
- Assert.IsNull(noPrice.Volume, "시세 미수집 종목은 null 로 반환된다");
- }
- [TestMethod]
- public async Task Quotes_Over50Codes_Fails()
- {
- // 기대 행위: 50개 초과 요청은 DB 조회 전에 거절한다.
- using var db = TestDb.Create();
- var codes = Enumerable.Range(100000, 51).Select(c => c.ToString()).ToList();
- var handler = new QuotesHandler(db);
- var result = await handler.Handle(new QuotesQuery(codes), default);
- Assert.IsFalse(result.IsSuccess, "최대 50개 제한을 넘으면 실패해야 한다");
- Assert.AreEqual("Stock.TooManyCodes", result.Error.Code);
- }
- }
|