StockWatchTests.cs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. using Domain.Entities.Members;
  2. using Domain.Entities.Stocks;
  3. using Domain.Entities.Stocks.ValueObject;
  4. using Infrastructure.Persistence;
  5. using Microsoft.EntityFrameworkCore;
  6. using ToggleCommand = Application.Features.Api.Stocks.Watch.Toggle.Command;
  7. using ToggleHandler = Application.Features.Api.Stocks.Watch.Toggle.Handler;
  8. using ListHandler = Application.Features.Api.Stocks.Watch.List.Handler;
  9. using ListQuery = Application.Features.Api.Stocks.Watch.List.Query;
  10. using IsWatchingHandler = Application.Features.Api.Stocks.Watch.IsWatching.Handler;
  11. using IsWatchingQuery = Application.Features.Api.Stocks.Watch.IsWatching.Query;
  12. namespace Application.Tests;
  13. /// <summary>
  14. /// 관심종목(StockWatch) 통합 테스트 — 토글 등록/해제(멱등), 미존재 종목 거부, 관심여부 맵, 목록 조인(denorm 시세).
  15. /// 공유 antooza_test DB — 종목코드 94xxxx 고범위 + GUID 회원으로 격리.
  16. /// </summary>
  17. [TestClass]
  18. public sealed class StockWatchTests
  19. {
  20. private static async Task<int> CreateMemberAsync(AppDbContext db)
  21. {
  22. var member = Member.Create($"watch-{Guid.NewGuid():N}@antooza.test");
  23. await db.Member.AddAsync(member);
  24. await db.SaveChangesAsync(default);
  25. return member.ID;
  26. }
  27. private static async Task<string> CreateStockAsync(AppDbContext db, string code, int? lastClose = null)
  28. {
  29. var stock = Stock.Create(code, $"관심테스트{code}", StockMarket.KOSPI, new DateOnly(2020, 1, 2));
  30. if (lastClose.HasValue)
  31. {
  32. stock.UpdateLastPrice(new DateOnly(2026, 7, 13), lastClose.Value, 1.5m, 1_000_000_000);
  33. }
  34. await db.Stock.AddAsync(stock);
  35. await db.SaveChangesAsync(default);
  36. return code;
  37. }
  38. private static string NextCode()
  39. {
  40. // 940000~949999 범위 (StockBoard 970xxx·Paper 95/9600xx 와 비충돌)
  41. var n = 940000 + Random.Shared.Next(0, 10000);
  42. return n.ToString("D6");
  43. }
  44. [TestMethod]
  45. public async Task Toggle_AddsThenRemoves_Idempotent()
  46. {
  47. var code = NextCode();
  48. int memberID;
  49. using (var db = TestDb.Create())
  50. {
  51. memberID = await CreateMemberAsync(db);
  52. await CreateStockAsync(db, code);
  53. }
  54. using (var db = TestDb.Create())
  55. {
  56. var r1 = await new ToggleHandler(db).Handle(new ToggleCommand(memberID, code), default);
  57. Assert.IsTrue(r1.IsSuccess);
  58. Assert.IsTrue(r1.Value.IsWatched, "최초 토글 → 등록");
  59. }
  60. using (var verify = TestDb.Create())
  61. {
  62. Assert.AreEqual(1, await verify.StockWatch.CountAsync(c => c.MemberID == memberID && c.StockCode == code));
  63. }
  64. using (var db = TestDb.Create())
  65. {
  66. var r2 = await new ToggleHandler(db).Handle(new ToggleCommand(memberID, code), default);
  67. Assert.IsTrue(r2.IsSuccess);
  68. Assert.IsFalse(r2.Value.IsWatched, "재토글 → 해제");
  69. }
  70. using (var verify = TestDb.Create())
  71. {
  72. Assert.AreEqual(0, await verify.StockWatch.CountAsync(c => c.MemberID == memberID && c.StockCode == code));
  73. }
  74. }
  75. [TestMethod]
  76. public async Task Toggle_UnknownStock_NotFound()
  77. {
  78. var code = NextCode();
  79. int memberID;
  80. using (var db = TestDb.Create())
  81. {
  82. memberID = await CreateMemberAsync(db);
  83. // 종목 미시드
  84. }
  85. using (var db = TestDb.Create())
  86. {
  87. var r = await new ToggleHandler(db).Handle(new ToggleCommand(memberID, code), default);
  88. Assert.IsTrue(r.IsFailure, "존재하지 않는 종목은 거부");
  89. Assert.AreEqual("StockWatch.StockNotFound", r.Error.Code);
  90. }
  91. }
  92. [TestMethod]
  93. public async Task IsWatching_ReturnsMap()
  94. {
  95. var watched = NextCode();
  96. var notWatched = NextCode();
  97. int memberID;
  98. using (var db = TestDb.Create())
  99. {
  100. memberID = await CreateMemberAsync(db);
  101. await CreateStockAsync(db, watched);
  102. await CreateStockAsync(db, notWatched);
  103. }
  104. using (var db = TestDb.Create())
  105. {
  106. await new ToggleHandler(db).Handle(new ToggleCommand(memberID, watched), default);
  107. }
  108. using (var verify = TestDb.Create())
  109. {
  110. var res = await new IsWatchingHandler(verify).Handle(new IsWatchingQuery(memberID, [watched, notWatched]), default);
  111. Assert.IsTrue(res.Map[watched], "등록 종목 true");
  112. Assert.IsFalse(res.Map[notWatched], "미등록 종목 false");
  113. }
  114. }
  115. [TestMethod]
  116. public async Task List_ReturnsWatched_WithDenormPrice()
  117. {
  118. var code = NextCode();
  119. int memberID;
  120. using (var db = TestDb.Create())
  121. {
  122. memberID = await CreateMemberAsync(db);
  123. await CreateStockAsync(db, code, lastClose: 73000);
  124. }
  125. using (var db = TestDb.Create())
  126. {
  127. await new ToggleHandler(db).Handle(new ToggleCommand(memberID, code), default);
  128. }
  129. using (var verify = TestDb.Create())
  130. {
  131. var res = await new ListHandler(verify).Handle(new ListQuery(memberID), default);
  132. var row = res.List.SingleOrDefault(c => c.Code == code);
  133. Assert.IsNotNull(row, "관심 등록 종목이 목록에 포함");
  134. Assert.AreEqual($"관심테스트{code}", row.Name);
  135. Assert.AreEqual(73000, row.ClosePrice);
  136. Assert.AreEqual("KOSPI", row.Market);
  137. }
  138. }
  139. }