| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- 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;
- /// <summary>
- /// D2 M3 수익인증 게시판 이미지 필수 검증 — BoardMeta.Write.RequireImage 게시판에서
- /// 이미지 없는 글은 거부(Post.ImageRequired), 본문 임베드 이미지가 있으면 허용됨을 검증한다.
- /// proof 보드는 BoardSeeder 로 lazy 시드해 RequireImage=true 로 provisioning 되는지도 함께 확인한다.
- /// 공유 antooza_test DB — GUID 회원으로 격리.
- /// </summary>
- [TestClass]
- public sealed class ProofBoardImageRequiredTests
- {
- private sealed class NullCacheService : ICacheService
- {
- public Task<T?> GetAsync<T>(string key, CancellationToken ct = default) => Task.FromResult<T?>(default);
- public Task SetAsync<T>(string key, T value, CancellationToken ct = default) => Task.CompletedTask;
- public Task SetAsync<T>(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<bool> TryLockAsync(string key, TimeSpan expiry, CancellationToken ct = default) => Task.FromResult(true);
- }
- // 모든 권한 허용 — 검증 대상은 이미지 필수 로직뿐.
- private sealed class AllowAllPermissionService : IBoardPermissionService
- {
- public Task<short> GetMemberPermissionLevelAsync(Member member, int boardID, CancellationToken ct) => Task.FromResult<short>(1000);
- public Task<bool> HasPermissionAsync(Member member, int boardID, short requiredPermission, CancellationToken ct) => Task.FromResult(true);
- public Task<BoardManager?> GetBoardManagerAsync(int boardID, int memberID, CancellationToken ct) => Task.FromResult<BoardManager?>(null);
- }
- // 이미지 필수 테스트는 본문 임베드 이미지로 통과시키므로 파일 저장은 호출되지 않는다.
- private sealed class UnusedFileStorage : IFileStorage
- {
- public Task<FileUploadResult?> SaveFileAsync(Microsoft.AspNetCore.Http.IFormFile? file, FileStoragePath path, string[] allowedExtensions, CancellationToken ct = default)
- => throw new System.NotSupportedException("테스트에서 파일 업로드 경로는 사용하지 않는다");
- public Task<FileUploadResult?> SaveBytesAsync(ReadOnlyMemory<byte> 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<int> 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, "수익 인증", "<p>인증 스샷</p><img src=\"https://cdn.antooza.test/p/1.png\" />", 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, "이미지 필수가 아닌 보드는 이미지 없어도 등록");
- }
- }
- }
|