ChatRoomTests.cs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. using Application.Abstractions.Chat;
  2. using Application.Helpers;
  3. using Domain.Entities.Chat;
  4. using HistoryHandler = Application.Features.Api.Chat.GetHistory.Handler;
  5. using HistoryQuery = Application.Features.Api.Chat.GetHistory.Query;
  6. namespace Application.Tests;
  7. [TestClass]
  8. public sealed class ChatRoomTests
  9. {
  10. // ── ChatRoomKeys 공개 계약 검증 ──────────────────────────────
  11. [TestMethod]
  12. public void RoomKeys_MainAndStock_AreValid()
  13. {
  14. // 기대 행위: 공개 계약 roomKey 는 main 과 stock:{6자리 숫자}만 허용한다.
  15. Assert.IsTrue(ChatRoomKeys.IsValid("main"));
  16. Assert.IsTrue(ChatRoomKeys.IsValid("stock:005930"));
  17. Assert.IsFalse(ChatRoomKeys.IsValid(null), "null 거부");
  18. Assert.IsFalse(ChatRoomKeys.IsValid(" "), "공백 거부");
  19. Assert.IsFalse(ChatRoomKeys.IsValid("MAIN"), "대문자 거부");
  20. Assert.IsFalse(ChatRoomKeys.IsValid("stock:12345"), "5자리 코드 거부");
  21. Assert.IsFalse(ChatRoomKeys.IsValid("stock:12345A"), "숫자 아닌 코드 거부");
  22. Assert.IsFalse(ChatRoomKeys.IsValid("channel:abcd"), "레거시 채널 키는 공개 계약이 아니다");
  23. Assert.IsFalse(ChatRoomKeys.IsValid("stock:0059301234567890"), "20자 초과 거부");
  24. }
  25. [TestMethod]
  26. public void RoomOpen_MainDefaultsOpen_StockRequiresActiveRow()
  27. {
  28. // 기대 행위: main 은 row 없어도 오픈, 종목방은 row 존재 + IsActive 필요 (단계 오픈 — d0 §5-④).
  29. var missing = new ChatRoomConfigLoader.RoomConfig(false, false, 0, null);
  30. var active = new ChatRoomConfigLoader.RoomConfig(true, true, 0, null);
  31. var inactive = new ChatRoomConfigLoader.RoomConfig(true, false, 0, null);
  32. Assert.IsTrue(ChatRoomConfigLoader.IsRoomOpen("main", missing), "main 은 기본 오픈");
  33. Assert.IsFalse(ChatRoomConfigLoader.IsRoomOpen("main", inactive), "main 도 명시 비활성 시 닫힘");
  34. Assert.IsFalse(ChatRoomConfigLoader.IsRoomOpen("stock:005930", missing), "종목방은 row 없으면 닫힘");
  35. Assert.IsTrue(ChatRoomConfigLoader.IsRoomOpen("stock:005930", active), "종목방은 활성 row 시 오픈");
  36. Assert.IsTrue(ChatRoomConfigLoader.IsRoomOpen("channel:legacy", missing), "레거시 채널 룸은 게이트 없음");
  37. }
  38. // ── 히스토리 API (익명 허용 — 게스트 읽기) ─────────────────
  39. [TestMethod]
  40. public async Task History_InvalidRoomKey_NotFound()
  41. {
  42. // 기대 행위: 계약 외 roomKey 는 NotFound 로 거절한다.
  43. using var db = TestDb.Create();
  44. var handler = new HistoryHandler(db, new FakeChatMessageStore());
  45. var result = await handler.Handle(new HistoryQuery("channel:hack", 50), default);
  46. Assert.IsFalse(result.IsSuccess);
  47. Assert.AreEqual("Chat.RoomNotFound", result.Error.Code);
  48. }
  49. [TestMethod]
  50. public async Task History_MainWithoutConfigRow_ReturnsOpenDefaults()
  51. {
  52. // 기대 행위: main 은 설정 row 없이도 조회 가능하며 기본값(슬로우모드 0, 공지 없음)을 반환한다.
  53. using var db = TestDb.Create();
  54. var store = new FakeChatMessageStore();
  55. await store.AddMessageAsync("main", CreateMessage(1, "첫 메시지"));
  56. await store.AddMessageAsync("main", CreateMessage(2, "둘째 메시지"));
  57. var handler = new HistoryHandler(db, store);
  58. var result = await handler.Handle(new HistoryQuery("main", 50), default);
  59. Assert.IsTrue(result.IsSuccess, "main 히스토리는 항상 조회 가능해야 한다");
  60. Assert.AreEqual("main", result.Value.RoomKey);
  61. Assert.AreEqual((short)0, result.Value.SlowModeSeconds);
  62. Assert.IsNull(result.Value.Notice);
  63. Assert.AreEqual(2, result.Value.Messages.Count);
  64. Assert.AreEqual("첫 메시지", result.Value.Messages[0].Content, "오래된 → 최신 순서");
  65. }
  66. [TestMethod]
  67. public async Task History_StockRoomWithoutConfig_NotFound()
  68. {
  69. // 기대 행위: 단계 오픈 전 종목방(설정 row 없음)은 NotFound.
  70. using var db = TestDb.Create();
  71. var handler = new HistoryHandler(db, new FakeChatMessageStore());
  72. var result = await handler.Handle(new HistoryQuery("stock:940001", 50), default);
  73. Assert.IsFalse(result.IsSuccess);
  74. Assert.AreEqual("Chat.RoomNotOpen", result.Error.Code);
  75. }
  76. [TestMethod]
  77. public async Task History_StockRoomActiveConfig_ReturnsMeta()
  78. {
  79. // 기대 행위: 활성 종목방은 공지/슬로우모드 메타와 메시지를 함께 반환한다.
  80. using var db = TestDb.Create();
  81. await db.ChatRoomConfig.AddAsync(ChatRoomConfig.Create("stock:940002", isActive: true, slowModeSeconds: 5, notice: "종목방 공지"));
  82. await db.SaveChangesAsync(default);
  83. var store = new FakeChatMessageStore();
  84. await store.AddMessageAsync("stock:940002", CreateMessage(1, "종목방 메시지"));
  85. var handler = new HistoryHandler(db, store);
  86. var result = await handler.Handle(new HistoryQuery("stock:940002", 50), default);
  87. Assert.IsTrue(result.IsSuccess);
  88. Assert.AreEqual((short)5, result.Value.SlowModeSeconds);
  89. Assert.AreEqual("종목방 공지", result.Value.Notice);
  90. Assert.AreEqual(1, result.Value.Messages.Count);
  91. }
  92. [TestMethod]
  93. public async Task History_Count_ClampedToMaxMessages()
  94. {
  95. // 기대 행위: count 는 1~ChatSettings.MaxMessages 로 클램프되고, 0 이하는 기본값(50)이 된다.
  96. using var db = TestDb.Create();
  97. var store = new FakeChatMessageStore();
  98. var handler = new HistoryHandler(db, store);
  99. await handler.Handle(new HistoryQuery("main", 99999), default);
  100. Assert.AreEqual(ChatSettings.MaxMessages, store.LastRequestedCount, "상한은 MaxMessages");
  101. await handler.Handle(new HistoryQuery("main", 0), default);
  102. Assert.AreEqual(HistoryHandler.DefaultCount, store.LastRequestedCount, "0 이하는 기본값");
  103. }
  104. // ── 채팅 제재 (ChatBan) — roomKey 체계 ───────────────────────
  105. [TestMethod]
  106. public async Task Ban_GlobalBan_BlocksAllRooms()
  107. {
  108. // 기대 행위: RoomKey NULL 제재는 모든 룸에서 발언을 차단한다.
  109. using var db = TestDb.Create();
  110. var memberID = await TestDb.CreateMemberAsync(db);
  111. await db.ChatBan.AddAsync(ChatBan.Create(memberID, null, "전체 제재", null, "admin"));
  112. await db.SaveChangesAsync(default);
  113. var now = DateTime.UtcNow;
  114. Assert.IsTrue(await ChatBanChecker.IsBannedAsync(db, memberID, "main", now, default));
  115. Assert.IsTrue(await ChatBanChecker.IsBannedAsync(db, memberID, "stock:005930", now, default));
  116. }
  117. [TestMethod]
  118. public async Task Ban_RoomBan_BlocksOnlyThatRoom()
  119. {
  120. // 기대 행위: 룸 지정 제재는 해당 룸만 차단하고 다른 룸 발언은 허용한다.
  121. using var db = TestDb.Create();
  122. var memberID = await TestDb.CreateMemberAsync(db);
  123. await db.ChatBan.AddAsync(ChatBan.Create(memberID, "main", "메인 룸 제재", null, "admin"));
  124. await db.SaveChangesAsync(default);
  125. var now = DateTime.UtcNow;
  126. Assert.IsTrue(await ChatBanChecker.IsBannedAsync(db, memberID, "main", now, default));
  127. Assert.IsFalse(await ChatBanChecker.IsBannedAsync(db, memberID, "stock:005930", now, default));
  128. }
  129. [TestMethod]
  130. public async Task Ban_Expired_DoesNotBlock()
  131. {
  132. // 기대 행위: 만료된 제재는 차단하지 않는다 (Lift 로 즉시 해제 포함).
  133. using var db = TestDb.Create();
  134. var memberID = await TestDb.CreateMemberAsync(db);
  135. var now = DateTime.UtcNow;
  136. await db.ChatBan.AddAsync(ChatBan.Create(memberID, null, "만료 제재", now.AddMinutes(-1), "admin"));
  137. await db.SaveChangesAsync(default);
  138. Assert.IsFalse(await ChatBanChecker.IsBannedAsync(db, memberID, "main", now, default));
  139. }
  140. // ── 테스트 헬퍼 ──────────────────────────────────────────────
  141. private static ChatMessage CreateMessage(int memberID, string content)
  142. {
  143. return new ChatMessage(memberID, $"SID{memberID:D4}", $"회원{memberID}", content, DateTime.UtcNow);
  144. }
  145. /// <summary>Redis 대체 인메모리 스토어 — 요청 count 기록으로 클램프 검증 지원</summary>
  146. private sealed class FakeChatMessageStore : IChatMessageStore
  147. {
  148. private readonly Dictionary<string, List<ChatMessage>> _rooms = [];
  149. public int LastRequestedCount { get; private set; }
  150. public Task AddMessageAsync(string roomKey, ChatMessage message)
  151. {
  152. if (!_rooms.TryGetValue(roomKey, out var list))
  153. {
  154. list = [];
  155. _rooms[roomKey] = list;
  156. }
  157. list.Add(message);
  158. return Task.CompletedTask;
  159. }
  160. public Task<IReadOnlyList<ChatMessage>> GetRecentMessagesAsync(string roomKey, int count)
  161. {
  162. LastRequestedCount = count;
  163. var list = _rooms.TryGetValue(roomKey, out var messages) ? messages : [];
  164. return Task.FromResult<IReadOnlyList<ChatMessage>>([..list.TakeLast(count)]);
  165. }
  166. public Task ClearRoomAsync(string roomKey)
  167. {
  168. _rooms.Remove(roomKey);
  169. return Task.CompletedTask;
  170. }
  171. }
  172. }