DailyBriefingTests.cs 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. using Application.Helpers;
  2. using Domain.Entities.Forum.Briefings;
  3. using Domain.Entities.Stocks;
  4. using Domain.Entities.Stocks.ValueObject;
  5. using Infrastructure.Persistence;
  6. using Microsoft.EntityFrameworkCore;
  7. namespace Application.Tests;
  8. /// <summary>
  9. /// D2 M3 장전 브리핑 자동 발행 통합 검증 — DailyBriefingComposer.
  10. /// 발행 시 briefing 보드 lazy 시드 + 시스템 계정(MemberID=null) IsNotice Post + DailyBriefing 기록,
  11. /// BriefingDate 멱등(같은 날 재실행 skip), 본문에 지수/등락/공시 취합을 검증한다.
  12. /// 공유 antooza_test DB — 종목코드는 98xxxx 고범위 + GUID 격리.
  13. /// </summary>
  14. [TestClass]
  15. public sealed class DailyBriefingTests
  16. {
  17. private static int _codeSeq = 980000;
  18. private static string NextCode() => System.Threading.Interlocked.Increment(ref _codeSeq).ToString("D6");
  19. private static async Task SeedIndexAsync(AppDbContext db, MarketIndexSeries series, string name, DateOnly date, decimal close, decimal changeVal, int flucRateBp)
  20. {
  21. await db.IndexDailyPrice.AddAsync(IndexDailyPrice.Create(series, name, date, close, changeVal, flucRateBp, close, close, close, 1000L, 1_000_000L));
  22. await db.SaveChangesAsync(default);
  23. }
  24. private static async Task<int> SeedStockWithPriceAsync(AppDbContext db, string code, string name, DateOnly date, int close, decimal changeRate)
  25. {
  26. var stock = Stock.Create(code, name, StockMarket.KOSPI, new DateOnly(2020, 1, 2));
  27. await db.Stock.AddAsync(stock);
  28. await db.SaveChangesAsync(default);
  29. await db.StockDailyPrice.AddAsync(StockDailyPrice.Create(stock.ID, date, close, close, close, close, 1000L, 1_000_000L, 0, changeRate));
  30. await db.SaveChangesAsync(default);
  31. return stock.ID;
  32. }
  33. [TestMethod]
  34. public async Task Publish_CreatesSystemNoticePost_AndBriefingRecord()
  35. {
  36. var date = new DateOnly(2026, 7, 6);
  37. using (var seed = TestDb.Create())
  38. {
  39. await SeedIndexAsync(seed, MarketIndexSeries.KOSPI, "코스피", new DateOnly(2026, 7, 3), 2800.55m, 12.30m, 44);
  40. }
  41. DailyBriefingComposer.PublishResult result;
  42. using (var db = TestDb.Create())
  43. {
  44. result = await DailyBriefingComposer.PublishAsync(db, date, default);
  45. }
  46. Assert.IsTrue(result.Published, "최초 발행은 Published");
  47. Assert.IsFalse(result.AlreadyExisted);
  48. using var verify = TestDb.Create();
  49. var briefing = await verify.DailyBriefing.AsNoTracking().SingleAsync(c => c.BriefingDate == date);
  50. Assert.AreEqual(result.PostID, briefing.PostID);
  51. var post = await verify.Post.AsNoTracking().SingleAsync(c => c.ID == result.PostID);
  52. Assert.IsNull(post.MemberID, "시스템 계정 = MemberID null");
  53. Assert.AreEqual(DailyBriefingComposer.SystemAuthorName, post.Name);
  54. Assert.IsTrue(post.IsNotice, "브리핑은 공지로 발행");
  55. StringAssert.Contains(post.Subject, "장전 브리핑");
  56. // briefing 보드가 lazy 시드됐는지
  57. var board = await verify.Board.AsNoTracking().SingleAsync(c => c.ID == post.BoardID);
  58. Assert.AreEqual(BoardSeeder.BriefingBoardCode, board.Code);
  59. }
  60. [TestMethod]
  61. public async Task Publish_Idempotent_SameDate_SkipsSecondRun()
  62. {
  63. var date = new DateOnly(2026, 7, 7);
  64. using (var seed = TestDb.Create())
  65. {
  66. await SeedIndexAsync(seed, MarketIndexSeries.KOSDAQ, "코스닥", new DateOnly(2026, 7, 3), 850.10m, -3.20m, -37);
  67. }
  68. int firstPostID;
  69. using (var db = TestDb.Create())
  70. {
  71. var r1 = await DailyBriefingComposer.PublishAsync(db, date, default);
  72. Assert.IsTrue(r1.Published);
  73. firstPostID = r1.PostID;
  74. }
  75. using (var db = TestDb.Create())
  76. {
  77. var r2 = await DailyBriefingComposer.PublishAsync(db, date, default);
  78. Assert.IsFalse(r2.Published, "같은 날 재실행은 발행하지 않는다");
  79. Assert.IsTrue(r2.AlreadyExisted);
  80. Assert.AreEqual(firstPostID, r2.PostID, "기존 PostID 를 반환");
  81. }
  82. using var verify = TestDb.Create();
  83. Assert.AreEqual(1, await verify.DailyBriefing.CountAsync(c => c.BriefingDate == date), "브리핑 기록은 1건뿐");
  84. }
  85. [TestMethod]
  86. public async Task Publish_ComposesIndicesMoversAndDisclosures_IntoBody()
  87. {
  88. var date = new DateOnly(2026, 12, 31);
  89. // 공유 DB 의 다른 테스트 시세보다 뒤 거래일이어야 movers 쿼리(글로벌 최신 거래일)가 이 종목을 잡는다.
  90. var tradeDate = new DateOnly(2026, 12, 30);
  91. var gainerCode = NextCode();
  92. var loserCode = NextCode();
  93. using (var db = TestDb.Create())
  94. {
  95. await SeedIndexAsync(db, MarketIndexSeries.KOSPI, "코스피", tradeDate, 2801.00m, 10.00m, 36);
  96. await SeedStockWithPriceAsync(db, gainerCode, "급등주식회사", tradeDate, close: 15000, changeRate: 12.34m);
  97. await SeedStockWithPriceAsync(db, loserCode, "급락주식회사", tradeDate, close: 8000, changeRate: -9.87m);
  98. await db.Disclosure.AddAsync(Disclosure.Create("20260703000001", "00000001", "공시법인", gainerCode, "Y", "주요사항보고서", "공시법인", tradeDate));
  99. await db.SaveChangesAsync(default);
  100. }
  101. int postID;
  102. using (var db = TestDb.Create())
  103. {
  104. var r = await DailyBriefingComposer.PublishAsync(db, date, default);
  105. postID = r.PostID;
  106. }
  107. using var verify = TestDb.Create();
  108. var body = (await verify.Post.AsNoTracking().SingleAsync(c => c.ID == postID)).Content;
  109. StringAssert.Contains(body, "전일 지수");
  110. StringAssert.Contains(body, "코스피");
  111. StringAssert.Contains(body, "등락 상위");
  112. StringAssert.Contains(body, "급등주식회사");
  113. StringAssert.Contains(body, "등락 하위");
  114. StringAssert.Contains(body, "급락주식회사");
  115. StringAssert.Contains(body, "주요 공시");
  116. StringAssert.Contains(body, "주요사항보고서");
  117. }
  118. }