using Application.Abstractions.Cache; using Application.Abstractions.Data; using Application.Abstractions.Forum; using Application.Abstractions.Messaging.Email; using Application.Helpers; using Domain.Entities.Common; using Domain.Entities.Common.ValueObject; using Domain.Entities.Forum.Boards; using Domain.Entities.Members; using Domain.Entities.Wallets; using Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; using SharedKernel.Storage; using CreateCommand = Application.Features.Api.Forum.Post.Create.Command; using CreateHandler = Application.Features.Api.Forum.Post.Create.Handler; namespace Application.Tests; /// /// D2 M3 수익인증 게시판 이미지 필수 검증 — BoardMeta.Write.RequireImage 게시판에서 /// 이미지 없는 글은 거부(Post.ImageRequired), 본문 임베드 이미지가 있으면 허용됨을 검증한다. /// proof 보드는 BoardSeeder 로 lazy 시드해 RequireImage=true 로 provisioning 되는지도 함께 확인한다. /// 공유 antooza_test DB — GUID 회원으로 격리. /// [TestClass] public sealed class ProofBoardImageRequiredTests { private sealed class NullCacheService : ICacheService { public Task GetAsync(string key, CancellationToken ct = default) => Task.FromResult(default); public Task SetAsync(string key, T value, CancellationToken ct = default) => Task.CompletedTask; public Task SetAsync(string key, T value, TimeSpan expiry, CancellationToken ct = default) => Task.CompletedTask; public Task RemoveAsync(string key, CancellationToken ct = default) => Task.CompletedTask; public Task RemoveByPrefixAsync(string prefix, CancellationToken ct = default) => Task.CompletedTask; public Task TryLockAsync(string key, TimeSpan expiry, CancellationToken ct = default) => Task.FromResult(true); } // 모든 권한 허용 — 검증 대상은 이미지 필수 로직뿐. private sealed class AllowAllPermissionService : IBoardPermissionService { public Task GetMemberPermissionLevelAsync(Member member, int boardID, CancellationToken ct) => Task.FromResult(1000); public Task HasPermissionAsync(Member member, int boardID, short requiredPermission, CancellationToken ct) => Task.FromResult(true); public Task GetBoardManagerAsync(int boardID, int memberID, CancellationToken ct) => Task.FromResult(null); } // 이미지 필수 테스트는 본문 임베드 이미지로 통과시키므로 파일 저장은 호출되지 않는다. private sealed class UnusedFileStorage : IFileStorage { public Task SaveFileAsync(Microsoft.AspNetCore.Http.IFormFile? file, FileStoragePath path, string[] allowedExtensions, CancellationToken ct = default) => throw new System.NotSupportedException("테스트에서 파일 업로드 경로는 사용하지 않는다"); public Task SaveBytesAsync(ReadOnlyMemory bytes, string extension, FileStoragePath path, CancellationToken ct = default) => throw new System.NotSupportedException(); public void DeleteByUrl(string? url) { } } private sealed class NullMailService : IMailService { public Task SendAsync(SendData data, CancellationToken ct = default) => Task.CompletedTask; } private static CreateHandler NewHandler(AppDbContext db) => new(db, new UnusedFileStorage(), new AllowAllPermissionService(), new NullMailService(), new NullCacheService()); private static async Task CreateMemberAsync(AppDbContext db) { var member = Member.Create($"proof-{Guid.NewGuid():N}@antooza.test"); await db.Member.AddAsync(member); await db.SaveChangesAsync(default); await db.MemberStats.AddAsync(MemberStats.Create(member.ID)); await db.Wallet.AddAsync(Wallet.Create(member.ID)); await db.SaveChangesAsync(default); return member.ID; } private static async Task SeedRewardConfigAsync(AppDbContext db) { var config = Config.Create(); config.UpdateReward(new RewardConfig { PostExp = 5, PostPoint = 10, MinPostLength = 0 }); await db.Config.AddAsync(config); await db.SaveChangesAsync(default); } [TestMethod] public async Task ProofBoard_SeededWithRequireImageTrue() { int boardID; using (var db = TestDb.Create()) { boardID = await BoardSeeder.ResolveProofBoardIDAsync(db, default); } using var verify = TestDb.Create(); var meta = await verify.BoardMeta.AsNoTracking().SingleAsync(c => c.BoardID == boardID); Assert.IsTrue(meta.Write.RequireImage, "proof 보드는 이미지 필수로 시드된다"); Assert.IsTrue(meta.Write.AllowImage); } [TestMethod] public async Task CreatePost_OnProofBoard_NoImage_Rejected() { int boardID; int memberID; using (var db = TestDb.Create()) { await SeedRewardConfigAsync(db); memberID = await CreateMemberAsync(db); boardID = await BoardSeeder.ResolveProofBoardIDAsync(db, default); } using (var db = TestDb.Create()) { var result = await NewHandler(db).Handle( new CreateCommand(memberID, boardID, null, "수익 자랑", "이미지 없는 본문입니다.", false, false, false, null, null, null, null), default); Assert.IsTrue(result.IsFailure, "이미지 필수 보드에서 이미지 없는 글은 거부"); Assert.AreEqual("Post.ImageRequired", result.Error.Code); } using (var verify = TestDb.Create()) { Assert.AreEqual(0, await verify.Post.CountAsync(c => c.BoardID == boardID), "거부 시 글이 생성되지 않는다"); } } [TestMethod] public async Task CreatePost_OnProofBoard_WithEmbeddedImage_Allowed() { int boardID; int memberID; using (var db = TestDb.Create()) { await SeedRewardConfigAsync(db); memberID = await CreateMemberAsync(db); boardID = await BoardSeeder.ResolveProofBoardIDAsync(db, default); } int postID; using (var db = TestDb.Create()) { var result = await NewHandler(db).Handle( new CreateCommand(memberID, boardID, null, "수익 인증", "

인증 스샷

", false, false, false, null, null, null, null), default); Assert.IsTrue(result.IsSuccess, "본문 임베드 이미지가 있으면 허용"); postID = result.Value; } using (var verify = TestDb.Create()) { Assert.IsTrue(await verify.Post.AnyAsync(c => c.ID == postID && c.BoardID == boardID), "글이 등록된다"); } } [TestMethod] public async Task CreatePost_OnNormalBoard_NoImage_Allowed() { // RequireImage=false 인 일반 보드는 이미지 없어도 통과 (회귀 방지) int boardID; int memberID; using (var db = TestDb.Create()) { await SeedRewardConfigAsync(db); memberID = await CreateMemberAsync(db); var group = new BoardGroup { Code = $"g{Guid.NewGuid():N}"[..12], Name = "그룹" }; await db.BoardGroup.AddAsync(group); await db.SaveChangesAsync(default); var board = new Board { BoardGroupID = group.ID, Code = $"b{Guid.NewGuid():N}"[..12], Name = "일반보드", IsActive = true }; await db.Board.AddAsync(board); await db.SaveChangesAsync(default); await db.BoardMeta.AddAsync(new BoardMeta { BoardID = board.ID }); await db.SaveChangesAsync(default); boardID = board.ID; } using (var db = TestDb.Create()) { var result = await NewHandler(db).Handle( new CreateCommand(memberID, boardID, null, "일반글", "이미지 없는 일반 본문", false, false, false, null, null, null, null), default); Assert.IsTrue(result.IsSuccess, "이미지 필수가 아닌 보드는 이미지 없어도 등록"); } } }