ProofBoardImageRequiredTests.cs 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. using Application.Abstractions.Cache;
  2. using Application.Abstractions.Data;
  3. using Application.Abstractions.Forum;
  4. using Application.Abstractions.Messaging.Email;
  5. using Application.Helpers;
  6. using Domain.Entities.Common;
  7. using Domain.Entities.Common.ValueObject;
  8. using Domain.Entities.Forum.Boards;
  9. using Domain.Entities.Members;
  10. using Domain.Entities.Wallets;
  11. using Infrastructure.Persistence;
  12. using Microsoft.EntityFrameworkCore;
  13. using SharedKernel.Storage;
  14. using CreateCommand = Application.Features.Api.Forum.Post.Create.Command;
  15. using CreateHandler = Application.Features.Api.Forum.Post.Create.Handler;
  16. namespace Application.Tests;
  17. /// <summary>
  18. /// D2 M3 수익인증 게시판 이미지 필수 검증 — BoardMeta.Write.RequireImage 게시판에서
  19. /// 이미지 없는 글은 거부(Post.ImageRequired), 본문 임베드 이미지가 있으면 허용됨을 검증한다.
  20. /// proof 보드는 BoardSeeder 로 lazy 시드해 RequireImage=true 로 provisioning 되는지도 함께 확인한다.
  21. /// 공유 antooza_test DB — GUID 회원으로 격리.
  22. /// </summary>
  23. [TestClass]
  24. public sealed class ProofBoardImageRequiredTests
  25. {
  26. private sealed class NullCacheService : ICacheService
  27. {
  28. public Task<T?> GetAsync<T>(string key, CancellationToken ct = default) => Task.FromResult<T?>(default);
  29. public Task SetAsync<T>(string key, T value, CancellationToken ct = default) => Task.CompletedTask;
  30. public Task SetAsync<T>(string key, T value, TimeSpan expiry, CancellationToken ct = default) => Task.CompletedTask;
  31. public Task RemoveAsync(string key, CancellationToken ct = default) => Task.CompletedTask;
  32. public Task RemoveByPrefixAsync(string prefix, CancellationToken ct = default) => Task.CompletedTask;
  33. public Task<bool> TryLockAsync(string key, TimeSpan expiry, CancellationToken ct = default) => Task.FromResult(true);
  34. }
  35. // 모든 권한 허용 — 검증 대상은 이미지 필수 로직뿐.
  36. private sealed class AllowAllPermissionService : IBoardPermissionService
  37. {
  38. public Task<short> GetMemberPermissionLevelAsync(Member member, int boardID, CancellationToken ct) => Task.FromResult<short>(1000);
  39. public Task<bool> HasPermissionAsync(Member member, int boardID, short requiredPermission, CancellationToken ct) => Task.FromResult(true);
  40. public Task<BoardManager?> GetBoardManagerAsync(int boardID, int memberID, CancellationToken ct) => Task.FromResult<BoardManager?>(null);
  41. }
  42. // 이미지 필수 테스트는 본문 임베드 이미지로 통과시키므로 파일 저장은 호출되지 않는다.
  43. private sealed class UnusedFileStorage : IFileStorage
  44. {
  45. public Task<FileUploadResult?> SaveFileAsync(Microsoft.AspNetCore.Http.IFormFile? file, FileStoragePath path, string[] allowedExtensions, CancellationToken ct = default)
  46. => throw new System.NotSupportedException("테스트에서 파일 업로드 경로는 사용하지 않는다");
  47. public Task<FileUploadResult?> SaveBytesAsync(ReadOnlyMemory<byte> bytes, string extension, FileStoragePath path, CancellationToken ct = default)
  48. => throw new System.NotSupportedException();
  49. public void DeleteByUrl(string? url) { }
  50. }
  51. private sealed class NullMailService : IMailService
  52. {
  53. public Task SendAsync(SendData data, CancellationToken ct = default) => Task.CompletedTask;
  54. }
  55. private static CreateHandler NewHandler(AppDbContext db) => new(db, new UnusedFileStorage(), new AllowAllPermissionService(), new NullMailService(), new NullCacheService());
  56. private static async Task<int> CreateMemberAsync(AppDbContext db)
  57. {
  58. var member = Member.Create($"proof-{Guid.NewGuid():N}@antooza.test");
  59. await db.Member.AddAsync(member);
  60. await db.SaveChangesAsync(default);
  61. await db.MemberStats.AddAsync(MemberStats.Create(member.ID));
  62. await db.Wallet.AddAsync(Wallet.Create(member.ID));
  63. await db.SaveChangesAsync(default);
  64. return member.ID;
  65. }
  66. private static async Task SeedRewardConfigAsync(AppDbContext db)
  67. {
  68. var config = Config.Create();
  69. config.UpdateReward(new RewardConfig { PostExp = 5, PostPoint = 10, MinPostLength = 0 });
  70. await db.Config.AddAsync(config);
  71. await db.SaveChangesAsync(default);
  72. }
  73. [TestMethod]
  74. public async Task ProofBoard_SeededWithRequireImageTrue()
  75. {
  76. int boardID;
  77. using (var db = TestDb.Create())
  78. {
  79. boardID = await BoardSeeder.ResolveProofBoardIDAsync(db, default);
  80. }
  81. using var verify = TestDb.Create();
  82. var meta = await verify.BoardMeta.AsNoTracking().SingleAsync(c => c.BoardID == boardID);
  83. Assert.IsTrue(meta.Write.RequireImage, "proof 보드는 이미지 필수로 시드된다");
  84. Assert.IsTrue(meta.Write.AllowImage);
  85. }
  86. [TestMethod]
  87. public async Task CreatePost_OnProofBoard_NoImage_Rejected()
  88. {
  89. int boardID;
  90. int memberID;
  91. using (var db = TestDb.Create())
  92. {
  93. await SeedRewardConfigAsync(db);
  94. memberID = await CreateMemberAsync(db);
  95. boardID = await BoardSeeder.ResolveProofBoardIDAsync(db, default);
  96. }
  97. using (var db = TestDb.Create())
  98. {
  99. var result = await NewHandler(db).Handle(
  100. new CreateCommand(memberID, boardID, null, "수익 자랑", "이미지 없는 본문입니다.", false, false, false, null, null, null, null),
  101. default);
  102. Assert.IsTrue(result.IsFailure, "이미지 필수 보드에서 이미지 없는 글은 거부");
  103. Assert.AreEqual("Post.ImageRequired", result.Error.Code);
  104. }
  105. using (var verify = TestDb.Create())
  106. {
  107. Assert.AreEqual(0, await verify.Post.CountAsync(c => c.BoardID == boardID), "거부 시 글이 생성되지 않는다");
  108. }
  109. }
  110. [TestMethod]
  111. public async Task CreatePost_OnProofBoard_WithEmbeddedImage_Allowed()
  112. {
  113. int boardID;
  114. int memberID;
  115. using (var db = TestDb.Create())
  116. {
  117. await SeedRewardConfigAsync(db);
  118. memberID = await CreateMemberAsync(db);
  119. boardID = await BoardSeeder.ResolveProofBoardIDAsync(db, default);
  120. }
  121. int postID;
  122. using (var db = TestDb.Create())
  123. {
  124. var result = await NewHandler(db).Handle(
  125. new CreateCommand(memberID, boardID, null, "수익 인증", "<p>인증 스샷</p><img src=\"https://cdn.antooza.test/p/1.png\" />", false, false, false, null, null, null, null),
  126. default);
  127. Assert.IsTrue(result.IsSuccess, "본문 임베드 이미지가 있으면 허용");
  128. postID = result.Value;
  129. }
  130. using (var verify = TestDb.Create())
  131. {
  132. Assert.IsTrue(await verify.Post.AnyAsync(c => c.ID == postID && c.BoardID == boardID), "글이 등록된다");
  133. }
  134. }
  135. [TestMethod]
  136. public async Task CreatePost_OnNormalBoard_NoImage_Allowed()
  137. {
  138. // RequireImage=false 인 일반 보드는 이미지 없어도 통과 (회귀 방지)
  139. int boardID;
  140. int memberID;
  141. using (var db = TestDb.Create())
  142. {
  143. await SeedRewardConfigAsync(db);
  144. memberID = await CreateMemberAsync(db);
  145. var group = new BoardGroup { Code = $"g{Guid.NewGuid():N}"[..12], Name = "그룹" };
  146. await db.BoardGroup.AddAsync(group);
  147. await db.SaveChangesAsync(default);
  148. var board = new Board { BoardGroupID = group.ID, Code = $"b{Guid.NewGuid():N}"[..12], Name = "일반보드", IsActive = true };
  149. await db.Board.AddAsync(board);
  150. await db.SaveChangesAsync(default);
  151. await db.BoardMeta.AddAsync(new BoardMeta { BoardID = board.ID });
  152. await db.SaveChangesAsync(default);
  153. boardID = board.ID;
  154. }
  155. using (var db = TestDb.Create())
  156. {
  157. var result = await NewHandler(db).Handle(
  158. new CreateCommand(memberID, boardID, null, "일반글", "이미지 없는 일반 본문", false, false, false, null, null, null, null),
  159. default);
  160. Assert.IsTrue(result.IsSuccess, "이미지 필수가 아닌 보드는 이미지 없어도 등록");
  161. }
  162. }
  163. }