| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162 |
- using Application.Abstractions.Cache;
- using Application.Helpers;
- using Domain.Entities.Common;
- using Domain.Entities.Members;
- using Domain.Entities.Wallets;
- using Domain.Entities.Wallets.ValueObject;
- using Microsoft.EntityFrameworkCore;
- namespace Application.Tests;
- [TestClass]
- public sealed class SignupRewardGranterTests
- {
- // 캐시는 항상 miss → DB Config 폴백 경로를 검증한다.
- 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 static async Task SeedConfigAsync(Infrastructure.Persistence.AppDbContext db, bool enabled, int coin, int cash, int exp)
- {
- var config = Config.Create();
- config.Update(
- config.Basic,
- config.Images,
- config.Meta,
- config.Company,
- config.Account,
- config.EmailTemplate,
- config.External,
- config.Crypto,
- config.Attendance,
- new SignupRewardConfig { Enabled = enabled, CoinAmount = coin, CashAmount = cash, ExpAmount = exp }
- );
- await db.Config.AddAsync(config);
- await db.SaveChangesAsync(default);
- }
- private static async Task<int> CreateMemberWithWalletAndStatsAsync(Infrastructure.Persistence.AppDbContext db)
- {
- var member = Member.Create($"signup-{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;
- }
- [TestMethod]
- public async Task Grant_CreditsCoinCashExp_OnEnabledConfig()
- {
- using var db = TestDb.Create();
- await SeedConfigAsync(db, enabled: true, coin: 500, cash: 300, exp: 20);
- var memberID = await CreateMemberWithWalletAndStatsAsync(db);
- await SignupRewardGranter.GrantAsync(db, new NullCacheService(), memberID, default);
- await db.SaveChangesAsync(default);
- var refID = SignupRewardGranter.RefIDFor(memberID);
- using var verify = TestDb.Create();
- var wallet = await verify.Wallet.Include(w => w.Balances).Include(w => w.Transactions).AsSplitQuery().SingleAsync(w => w.MemberID == memberID);
- Assert.AreEqual(500m, wallet.Balances.Single(b => b.Type == WalletBalanceType.Airdrop).Amount.Value, "코인은 Airdrop 파티션에 적립되어야 한다");
- Assert.AreEqual(300m, wallet.Balances.Single(b => b.Type == WalletBalanceType.Adjusted).Amount.Value, "캐시는 Adjusted 파티션에 적립되어야 한다");
- var coinTx = wallet.Transactions.Single(t => t.BalanceType == WalletBalanceType.Airdrop && t.RefID == refID);
- var cashTx = wallet.Transactions.Single(t => t.BalanceType == WalletBalanceType.Adjusted && t.RefID == refID);
- Assert.AreEqual(WalletTransactionType.RewardEarned, coinTx.TxType, "멱등 판정 일원화를 위해 TxType 은 RewardEarned 여야 한다");
- Assert.AreEqual(WalletTransactionType.RewardEarned, cashTx.TxType);
- var stats = await verify.MemberStats.SingleAsync(s => s.MemberID == memberID);
- Assert.AreEqual(20L, stats.Exp, "XP 는 MemberStats.Exp 에 반영되어야 한다");
- var expLog = await verify.MemberExpLog.Where(l => l.MemberID == memberID).ToListAsync();
- Assert.AreEqual(1, expLog.Count, "가입 XP 로그가 1건 남아야 한다");
- Assert.AreEqual(20, expLog[0].Amount);
- }
- [TestMethod]
- public async Task Grant_IsIdempotent_NoDoubleCredit()
- {
- using var db = TestDb.Create();
- await SeedConfigAsync(db, enabled: true, coin: 500, cash: 0, exp: 10);
- var memberID = await CreateMemberWithWalletAndStatsAsync(db);
- // 1차 지급
- await SignupRewardGranter.GrantAsync(db, new NullCacheService(), memberID, default);
- await db.SaveChangesAsync(default);
- // 2차 재호출 (신규 컨텍스트 — 실제 재가입/재로그인 시뮬레이션)
- using var db2 = TestDb.Create();
- await SignupRewardGranter.GrantAsync(db2, new NullCacheService(), memberID, default);
- await db2.SaveChangesAsync(default);
- var refID = SignupRewardGranter.RefIDFor(memberID);
- using var verify = TestDb.Create();
- var wallet = await verify.Wallet.Include(w => w.Balances).Include(w => w.Transactions).AsSplitQuery().SingleAsync(w => w.MemberID == memberID);
- Assert.AreEqual(500m, wallet.Balances.Single(b => b.Type == WalletBalanceType.Airdrop).Amount.Value, "재호출해도 코인은 이중 적립되지 않아야 한다");
- Assert.AreEqual(1, wallet.Transactions.Count(t => t.RefID == refID), "가입 보상 원장은 정확히 1건이어야 한다");
- var stats = await verify.MemberStats.SingleAsync(s => s.MemberID == memberID);
- Assert.AreEqual(10L, stats.Exp, "XP 도 이중 지급되지 않아야 한다");
- var expLogCount = await verify.MemberExpLog.CountAsync(l => l.MemberID == memberID);
- Assert.AreEqual(1, expLogCount, "XP 로그도 1건만 남아야 한다");
- }
- [TestMethod]
- public async Task Grant_DisabledConfig_GrantsNothing()
- {
- using var db = TestDb.Create();
- await SeedConfigAsync(db, enabled: false, coin: 500, cash: 300, exp: 20);
- var memberID = await CreateMemberWithWalletAndStatsAsync(db);
- await SignupRewardGranter.GrantAsync(db, new NullCacheService(), memberID, default);
- await db.SaveChangesAsync(default);
- var refID = SignupRewardGranter.RefIDFor(memberID);
- using var verify = TestDb.Create();
- var wallet = await verify.Wallet.Include(w => w.Balances).Include(w => w.Transactions).AsSplitQuery().SingleAsync(w => w.MemberID == memberID);
- Assert.AreEqual(0m, wallet.Balances.Single(b => b.Type == WalletBalanceType.Airdrop).Amount.Value, "비활성 시 코인 미지급");
- Assert.AreEqual(0m, wallet.Balances.Single(b => b.Type == WalletBalanceType.Adjusted).Amount.Value, "비활성 시 캐시 미지급");
- Assert.AreEqual(0, wallet.Transactions.Count(t => t.RefID == refID), "비활성 시 원장 미기록");
- var stats = await verify.MemberStats.SingleAsync(s => s.MemberID == memberID);
- Assert.AreEqual(0L, stats.Exp, "비활성 시 XP 미지급");
- }
- [TestMethod]
- public async Task Grant_AllZeroAmounts_GrantsNothing_EvenIfEnabled()
- {
- using var db = TestDb.Create();
- await SeedConfigAsync(db, enabled: true, coin: 0, cash: 0, exp: 0);
- var memberID = await CreateMemberWithWalletAndStatsAsync(db);
- await SignupRewardGranter.GrantAsync(db, new NullCacheService(), memberID, default);
- await db.SaveChangesAsync(default);
- var refID = SignupRewardGranter.RefIDFor(memberID);
- using var verify = TestDb.Create();
- var txCount = await verify.WalletTransaction.CountAsync(t => t.RefID == refID);
- Assert.AreEqual(0, txCount, "활성이라도 전액 0 이면 아무것도 지급하지 않는다");
- var stats = await verify.MemberStats.SingleAsync(s => s.MemberID == memberID);
- Assert.AreEqual(0L, stats.Exp);
- }
- }
|