using Application.Helpers; using Domain.Entities.Forum.Briefings; using Domain.Entities.Stocks; using Domain.Entities.Stocks.ValueObject; using Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; namespace Application.Tests; /// /// D2 M3 장전 브리핑 자동 발행 통합 검증 — DailyBriefingComposer. /// 발행 시 briefing 보드 lazy 시드 + 시스템 계정(MemberID=null) IsNotice Post + DailyBriefing 기록, /// BriefingDate 멱등(같은 날 재실행 skip), 본문에 지수/등락/공시 취합을 검증한다. /// 공유 antooza_test DB — 종목코드는 98xxxx 고범위 + GUID 격리. /// [TestClass] public sealed class DailyBriefingTests { private static int _codeSeq = 980000; private static string NextCode() => System.Threading.Interlocked.Increment(ref _codeSeq).ToString("D6"); private static async Task SeedIndexAsync(AppDbContext db, MarketIndexSeries series, string name, DateOnly date, decimal close, decimal changeVal, int flucRateBp) { await db.IndexDailyPrice.AddAsync(IndexDailyPrice.Create(series, name, date, close, changeVal, flucRateBp, close, close, close, 1000L, 1_000_000L)); await db.SaveChangesAsync(default); } private static async Task SeedStockWithPriceAsync(AppDbContext db, string code, string name, DateOnly date, int close, decimal changeRate) { var stock = Stock.Create(code, name, StockMarket.KOSPI, new DateOnly(2020, 1, 2)); await db.Stock.AddAsync(stock); await db.SaveChangesAsync(default); await db.StockDailyPrice.AddAsync(StockDailyPrice.Create(stock.ID, date, close, close, close, close, 1000L, 1_000_000L, 0, changeRate)); await db.SaveChangesAsync(default); return stock.ID; } [TestMethod] public async Task Publish_CreatesSystemNoticePost_AndBriefingRecord() { var date = new DateOnly(2026, 7, 6); using (var seed = TestDb.Create()) { await SeedIndexAsync(seed, MarketIndexSeries.KOSPI, "코스피", new DateOnly(2026, 7, 3), 2800.55m, 12.30m, 44); } DailyBriefingComposer.PublishResult result; using (var db = TestDb.Create()) { result = await DailyBriefingComposer.PublishAsync(db, date, default); } Assert.IsTrue(result.Published, "최초 발행은 Published"); Assert.IsFalse(result.AlreadyExisted); using var verify = TestDb.Create(); var briefing = await verify.DailyBriefing.AsNoTracking().SingleAsync(c => c.BriefingDate == date); Assert.AreEqual(result.PostID, briefing.PostID); var post = await verify.Post.AsNoTracking().SingleAsync(c => c.ID == result.PostID); Assert.IsNull(post.MemberID, "시스템 계정 = MemberID null"); Assert.AreEqual(DailyBriefingComposer.SystemAuthorName, post.Name); Assert.IsTrue(post.IsNotice, "브리핑은 공지로 발행"); StringAssert.Contains(post.Subject, "장전 시황"); // briefing 보드가 lazy 시드됐는지 var board = await verify.Board.AsNoTracking().SingleAsync(c => c.ID == post.BoardID); Assert.AreEqual(BoardSeeder.BriefingBoardCode, board.Code); } [TestMethod] public async Task Publish_Idempotent_SameDate_SkipsSecondRun() { var date = new DateOnly(2026, 7, 7); using (var seed = TestDb.Create()) { await SeedIndexAsync(seed, MarketIndexSeries.KOSDAQ, "코스닥", new DateOnly(2026, 7, 3), 850.10m, -3.20m, -37); } int firstPostID; using (var db = TestDb.Create()) { var r1 = await DailyBriefingComposer.PublishAsync(db, date, default); Assert.IsTrue(r1.Published); firstPostID = r1.PostID; } using (var db = TestDb.Create()) { var r2 = await DailyBriefingComposer.PublishAsync(db, date, default); Assert.IsFalse(r2.Published, "같은 날 재실행은 발행하지 않는다"); Assert.IsTrue(r2.AlreadyExisted); Assert.AreEqual(firstPostID, r2.PostID, "기존 PostID 를 반환"); } using var verify = TestDb.Create(); Assert.AreEqual(1, await verify.DailyBriefing.CountAsync(c => c.BriefingDate == date), "브리핑 기록은 1건뿐"); } [TestMethod] public async Task Publish_ComposesIndicesMoversAndDisclosures_IntoBody() { var date = new DateOnly(2026, 12, 31); // 공유 DB 의 다른 테스트 시세보다 뒤 거래일이어야 movers 쿼리(글로벌 최신 거래일)가 이 종목을 잡는다. var tradeDate = new DateOnly(2026, 12, 30); var gainerCode = NextCode(); var loserCode = NextCode(); using (var db = TestDb.Create()) { await SeedIndexAsync(db, MarketIndexSeries.KOSPI, "코스피", tradeDate, 2801.00m, 10.00m, 36); await SeedStockWithPriceAsync(db, gainerCode, "급등주식회사", tradeDate, close: 15000, changeRate: 12.34m); await SeedStockWithPriceAsync(db, loserCode, "급락주식회사", tradeDate, close: 8000, changeRate: -9.87m); await db.Disclosure.AddAsync(Disclosure.Create("20260703000001", "00000001", "공시법인", gainerCode, "Y", "주요사항보고서", "공시법인", tradeDate)); await db.SaveChangesAsync(default); } int postID; using (var db = TestDb.Create()) { var r = await DailyBriefingComposer.PublishAsync(db, date, default); postID = r.PostID; } using var verify = TestDb.Create(); var body = (await verify.Post.AsNoTracking().SingleAsync(c => c.ID == postID)).Content; StringAssert.Contains(body, "전일 지수"); StringAssert.Contains(body, "코스피"); StringAssert.Contains(body, "등락 상위"); StringAssert.Contains(body, "급등주식회사"); StringAssert.Contains(body, "등락 하위"); StringAssert.Contains(body, "급락주식회사"); StringAssert.Contains(body, "주요 공시"); StringAssert.Contains(body, "주요사항보고서"); } }