using Domain.Entities.Members; using Domain.Entities.Stocks; using Domain.Entities.Stocks.ValueObject; using Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; using ToggleCommand = Application.Features.Api.Stocks.Watch.Toggle.Command; using ToggleHandler = Application.Features.Api.Stocks.Watch.Toggle.Handler; using ListHandler = Application.Features.Api.Stocks.Watch.List.Handler; using ListQuery = Application.Features.Api.Stocks.Watch.List.Query; using IsWatchingHandler = Application.Features.Api.Stocks.Watch.IsWatching.Handler; using IsWatchingQuery = Application.Features.Api.Stocks.Watch.IsWatching.Query; namespace Application.Tests; /// /// 관심종목(StockWatch) 통합 테스트 — 토글 등록/해제(멱등), 미존재 종목 거부, 관심여부 맵, 목록 조인(denorm 시세). /// 공유 antooza_test DB — 종목코드 94xxxx 고범위 + GUID 회원으로 격리. /// [TestClass] public sealed class StockWatchTests { private static async Task CreateMemberAsync(AppDbContext db) { var member = Member.Create($"watch-{Guid.NewGuid():N}@antooza.test"); await db.Member.AddAsync(member); await db.SaveChangesAsync(default); return member.ID; } private static async Task CreateStockAsync(AppDbContext db, string code, int? lastClose = null) { var stock = Stock.Create(code, $"관심테스트{code}", StockMarket.KOSPI, new DateOnly(2020, 1, 2)); if (lastClose.HasValue) { stock.UpdateLastPrice(new DateOnly(2026, 7, 13), lastClose.Value, 1.5m, 1_000_000_000); } await db.Stock.AddAsync(stock); await db.SaveChangesAsync(default); return code; } private static string NextCode() { // 940000~949999 범위 (StockBoard 970xxx·Paper 95/9600xx 와 비충돌) var n = 940000 + Random.Shared.Next(0, 10000); return n.ToString("D6"); } [TestMethod] public async Task Toggle_AddsThenRemoves_Idempotent() { var code = NextCode(); int memberID; using (var db = TestDb.Create()) { memberID = await CreateMemberAsync(db); await CreateStockAsync(db, code); } using (var db = TestDb.Create()) { var r1 = await new ToggleHandler(db).Handle(new ToggleCommand(memberID, code), default); Assert.IsTrue(r1.IsSuccess); Assert.IsTrue(r1.Value.IsWatched, "최초 토글 → 등록"); } using (var verify = TestDb.Create()) { Assert.AreEqual(1, await verify.StockWatch.CountAsync(c => c.MemberID == memberID && c.StockCode == code)); } using (var db = TestDb.Create()) { var r2 = await new ToggleHandler(db).Handle(new ToggleCommand(memberID, code), default); Assert.IsTrue(r2.IsSuccess); Assert.IsFalse(r2.Value.IsWatched, "재토글 → 해제"); } using (var verify = TestDb.Create()) { Assert.AreEqual(0, await verify.StockWatch.CountAsync(c => c.MemberID == memberID && c.StockCode == code)); } } [TestMethod] public async Task Toggle_UnknownStock_NotFound() { var code = NextCode(); int memberID; using (var db = TestDb.Create()) { memberID = await CreateMemberAsync(db); // 종목 미시드 } using (var db = TestDb.Create()) { var r = await new ToggleHandler(db).Handle(new ToggleCommand(memberID, code), default); Assert.IsTrue(r.IsFailure, "존재하지 않는 종목은 거부"); Assert.AreEqual("StockWatch.StockNotFound", r.Error.Code); } } [TestMethod] public async Task IsWatching_ReturnsMap() { var watched = NextCode(); var notWatched = NextCode(); int memberID; using (var db = TestDb.Create()) { memberID = await CreateMemberAsync(db); await CreateStockAsync(db, watched); await CreateStockAsync(db, notWatched); } using (var db = TestDb.Create()) { await new ToggleHandler(db).Handle(new ToggleCommand(memberID, watched), default); } using (var verify = TestDb.Create()) { var res = await new IsWatchingHandler(verify).Handle(new IsWatchingQuery(memberID, [watched, notWatched]), default); Assert.IsTrue(res.Map[watched], "등록 종목 true"); Assert.IsFalse(res.Map[notWatched], "미등록 종목 false"); } } [TestMethod] public async Task List_ReturnsWatched_WithDenormPrice() { var code = NextCode(); int memberID; using (var db = TestDb.Create()) { memberID = await CreateMemberAsync(db); await CreateStockAsync(db, code, lastClose: 73000); } using (var db = TestDb.Create()) { await new ToggleHandler(db).Handle(new ToggleCommand(memberID, code), default); } using (var verify = TestDb.Create()) { var res = await new ListHandler(verify).Handle(new ListQuery(memberID), default); var row = res.List.SingleOrDefault(c => c.Code == code); Assert.IsNotNull(row, "관심 등록 종목이 목록에 포함"); Assert.AreEqual($"관심테스트{code}", row.Name); Assert.AreEqual(73000, row.ClosePrice); Assert.AreEqual("KOSPI", row.Market); } } }