| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- using Application.Abstractions.Chat;
- using Application.Helpers;
- using Domain.Entities.Chat;
- using HistoryHandler = Application.Features.Api.Chat.GetHistory.Handler;
- using HistoryQuery = Application.Features.Api.Chat.GetHistory.Query;
- namespace Application.Tests;
- [TestClass]
- public sealed class ChatRoomTests
- {
- // ── ChatRoomKeys 공개 계약 검증 ──────────────────────────────
- [TestMethod]
- public void RoomKeys_MainAndStock_AreValid()
- {
- // 기대 행위: 공개 계약 roomKey 는 main 과 stock:{6자리 숫자}만 허용한다.
- Assert.IsTrue(ChatRoomKeys.IsValid("main"));
- Assert.IsTrue(ChatRoomKeys.IsValid("stock:005930"));
- Assert.IsFalse(ChatRoomKeys.IsValid(null), "null 거부");
- Assert.IsFalse(ChatRoomKeys.IsValid(" "), "공백 거부");
- Assert.IsFalse(ChatRoomKeys.IsValid("MAIN"), "대문자 거부");
- Assert.IsFalse(ChatRoomKeys.IsValid("stock:12345"), "5자리 코드 거부");
- Assert.IsFalse(ChatRoomKeys.IsValid("stock:12345A"), "숫자 아닌 코드 거부");
- Assert.IsFalse(ChatRoomKeys.IsValid("channel:abcd"), "레거시 채널 키는 공개 계약이 아니다");
- Assert.IsFalse(ChatRoomKeys.IsValid("stock:0059301234567890"), "20자 초과 거부");
- }
- [TestMethod]
- public void RoomOpen_MainDefaultsOpen_StockRequiresActiveRow()
- {
- // 기대 행위: main 은 row 없어도 오픈, 종목방은 row 존재 + IsActive 필요 (단계 오픈 — d0 §5-④).
- var missing = new ChatRoomConfigLoader.RoomConfig(false, false, 0, null);
- var active = new ChatRoomConfigLoader.RoomConfig(true, true, 0, null);
- var inactive = new ChatRoomConfigLoader.RoomConfig(true, false, 0, null);
- Assert.IsTrue(ChatRoomConfigLoader.IsRoomOpen("main", missing), "main 은 기본 오픈");
- Assert.IsFalse(ChatRoomConfigLoader.IsRoomOpen("main", inactive), "main 도 명시 비활성 시 닫힘");
- Assert.IsFalse(ChatRoomConfigLoader.IsRoomOpen("stock:005930", missing), "종목방은 row 없으면 닫힘");
- Assert.IsTrue(ChatRoomConfigLoader.IsRoomOpen("stock:005930", active), "종목방은 활성 row 시 오픈");
- Assert.IsTrue(ChatRoomConfigLoader.IsRoomOpen("channel:legacy", missing), "레거시 채널 룸은 게이트 없음");
- }
- // ── 히스토리 API (익명 허용 — 게스트 읽기) ─────────────────
- [TestMethod]
- public async Task History_InvalidRoomKey_NotFound()
- {
- // 기대 행위: 계약 외 roomKey 는 NotFound 로 거절한다.
- using var db = TestDb.Create();
- var handler = new HistoryHandler(db, new FakeChatMessageStore());
- var result = await handler.Handle(new HistoryQuery("channel:hack", 50), default);
- Assert.IsFalse(result.IsSuccess);
- Assert.AreEqual("Chat.RoomNotFound", result.Error.Code);
- }
- [TestMethod]
- public async Task History_MainWithoutConfigRow_ReturnsOpenDefaults()
- {
- // 기대 행위: main 은 설정 row 없이도 조회 가능하며 기본값(슬로우모드 0, 공지 없음)을 반환한다.
- using var db = TestDb.Create();
- var store = new FakeChatMessageStore();
- await store.AddMessageAsync("main", CreateMessage(1, "첫 메시지"));
- await store.AddMessageAsync("main", CreateMessage(2, "둘째 메시지"));
- var handler = new HistoryHandler(db, store);
- var result = await handler.Handle(new HistoryQuery("main", 50), default);
- Assert.IsTrue(result.IsSuccess, "main 히스토리는 항상 조회 가능해야 한다");
- Assert.AreEqual("main", result.Value.RoomKey);
- Assert.AreEqual((short)0, result.Value.SlowModeSeconds);
- Assert.IsNull(result.Value.Notice);
- Assert.AreEqual(2, result.Value.Messages.Count);
- Assert.AreEqual("첫 메시지", result.Value.Messages[0].Content, "오래된 → 최신 순서");
- }
- [TestMethod]
- public async Task History_StockRoomWithoutConfig_NotFound()
- {
- // 기대 행위: 단계 오픈 전 종목방(설정 row 없음)은 NotFound.
- using var db = TestDb.Create();
- var handler = new HistoryHandler(db, new FakeChatMessageStore());
- var result = await handler.Handle(new HistoryQuery("stock:940001", 50), default);
- Assert.IsFalse(result.IsSuccess);
- Assert.AreEqual("Chat.RoomNotOpen", result.Error.Code);
- }
- [TestMethod]
- public async Task History_StockRoomActiveConfig_ReturnsMeta()
- {
- // 기대 행위: 활성 종목방은 공지/슬로우모드 메타와 메시지를 함께 반환한다.
- using var db = TestDb.Create();
- await db.ChatRoomConfig.AddAsync(ChatRoomConfig.Create("stock:940002", isActive: true, slowModeSeconds: 5, notice: "종목방 공지"));
- await db.SaveChangesAsync(default);
- var store = new FakeChatMessageStore();
- await store.AddMessageAsync("stock:940002", CreateMessage(1, "종목방 메시지"));
- var handler = new HistoryHandler(db, store);
- var result = await handler.Handle(new HistoryQuery("stock:940002", 50), default);
- Assert.IsTrue(result.IsSuccess);
- Assert.AreEqual((short)5, result.Value.SlowModeSeconds);
- Assert.AreEqual("종목방 공지", result.Value.Notice);
- Assert.AreEqual(1, result.Value.Messages.Count);
- }
- [TestMethod]
- public async Task History_Count_ClampedToMaxMessages()
- {
- // 기대 행위: count 는 1~ChatSettings.MaxMessages 로 클램프되고, 0 이하는 기본값(50)이 된다.
- using var db = TestDb.Create();
- var store = new FakeChatMessageStore();
- var handler = new HistoryHandler(db, store);
- await handler.Handle(new HistoryQuery("main", 99999), default);
- Assert.AreEqual(ChatSettings.MaxMessages, store.LastRequestedCount, "상한은 MaxMessages");
- await handler.Handle(new HistoryQuery("main", 0), default);
- Assert.AreEqual(HistoryHandler.DefaultCount, store.LastRequestedCount, "0 이하는 기본값");
- }
- // ── 채팅 제재 (ChatBan) — roomKey 체계 ───────────────────────
- [TestMethod]
- public async Task Ban_GlobalBan_BlocksAllRooms()
- {
- // 기대 행위: RoomKey NULL 제재는 모든 룸에서 발언을 차단한다.
- using var db = TestDb.Create();
- var memberID = await TestDb.CreateMemberAsync(db);
- await db.ChatBan.AddAsync(ChatBan.Create(memberID, null, "전체 제재", null, "admin"));
- await db.SaveChangesAsync(default);
- var now = DateTime.UtcNow;
- Assert.IsTrue(await ChatBanChecker.IsBannedAsync(db, memberID, "main", now, default));
- Assert.IsTrue(await ChatBanChecker.IsBannedAsync(db, memberID, "stock:005930", now, default));
- }
- [TestMethod]
- public async Task Ban_RoomBan_BlocksOnlyThatRoom()
- {
- // 기대 행위: 룸 지정 제재는 해당 룸만 차단하고 다른 룸 발언은 허용한다.
- using var db = TestDb.Create();
- var memberID = await TestDb.CreateMemberAsync(db);
- await db.ChatBan.AddAsync(ChatBan.Create(memberID, "main", "메인 룸 제재", null, "admin"));
- await db.SaveChangesAsync(default);
- var now = DateTime.UtcNow;
- Assert.IsTrue(await ChatBanChecker.IsBannedAsync(db, memberID, "main", now, default));
- Assert.IsFalse(await ChatBanChecker.IsBannedAsync(db, memberID, "stock:005930", now, default));
- }
- [TestMethod]
- public async Task Ban_Expired_DoesNotBlock()
- {
- // 기대 행위: 만료된 제재는 차단하지 않는다 (Lift 로 즉시 해제 포함).
- using var db = TestDb.Create();
- var memberID = await TestDb.CreateMemberAsync(db);
- var now = DateTime.UtcNow;
- await db.ChatBan.AddAsync(ChatBan.Create(memberID, null, "만료 제재", now.AddMinutes(-1), "admin"));
- await db.SaveChangesAsync(default);
- Assert.IsFalse(await ChatBanChecker.IsBannedAsync(db, memberID, "main", now, default));
- }
- // ── 테스트 헬퍼 ──────────────────────────────────────────────
- private static ChatMessage CreateMessage(int memberID, string content)
- {
- return new ChatMessage(memberID, $"SID{memberID:D4}", $"회원{memberID}", content, DateTime.UtcNow);
- }
- /// <summary>Redis 대체 인메모리 스토어 — 요청 count 기록으로 클램프 검증 지원</summary>
- private sealed class FakeChatMessageStore : IChatMessageStore
- {
- private readonly Dictionary<string, List<ChatMessage>> _rooms = [];
- public int LastRequestedCount { get; private set; }
- public Task AddMessageAsync(string roomKey, ChatMessage message)
- {
- if (!_rooms.TryGetValue(roomKey, out var list))
- {
- list = [];
- _rooms[roomKey] = list;
- }
- list.Add(message);
- return Task.CompletedTask;
- }
- public Task<IReadOnlyList<ChatMessage>> GetRecentMessagesAsync(string roomKey, int count)
- {
- LastRequestedCount = count;
- var list = _rooms.TryGetValue(roomKey, out var messages) ? messages : [];
- return Task.FromResult<IReadOnlyList<ChatMessage>>([..list.TakeLast(count)]);
- }
- public Task ClearRoomAsync(string roomKey)
- {
- _rooms.Remove(roomKey);
- return Task.CompletedTask;
- }
- }
- }
|