using Domain.Entities.Common.ValueObject; using Domain.Entities.Wallets; using Microsoft.EntityFrameworkCore; namespace Application.Tests; [TestClass] public sealed class WalletBalanceConcurrencyTests { [TestMethod] public async Task ConcurrentUpdate_OnSameBalance_ThrowsConcurrencyException() { // P0 #8 재현: WalletBalance 에 동시성 토큰이 없어 동시 갱신이 last-write-wins 로 // 조용히 덮어써졌다 (이중 지출 가능). 기대 행위: 두 번째 저장은 DbUpdateConcurrencyException. int memberID; using (var setup = TestDb.Create()) { memberID = await TestDb.CreateMemberAsync(setup); var wallet = Wallet.Create(memberID); wallet.CreditPgCharge(Money.KRW(10000), "seed"); await setup.Wallet.AddAsync(wallet); await setup.SaveChangesAsync(default); } using var db1 = TestDb.Create(); using var db2 = TestDb.Create(); var w1 = await db1.Wallet.Include(w => w.Balances).AsSplitQuery().SingleAsync(w => w.MemberID == memberID); var w2 = await db2.Wallet.Include(w => w.Balances).AsSplitQuery().SingleAsync(w => w.MemberID == memberID); w1.CreditPgCharge(Money.KRW(1000), "c1"); w2.CreditPgCharge(Money.KRW(1000), "c2"); await db1.SaveChangesAsync(default); await Assert.ThrowsExceptionAsync(() => db2.SaveChangesAsync(default)); } }